#!/usr/bin/env python3

'''
Support for generating C++ and python wrappers for the mupdf API.

Args:

    --b     [<args>] <actions>:
    --build [<args>] <actions>:
        Builds some or all of the C++ and python interfaces.

        By default we create source files in:
            mupdf/platform/c++/
            mupdf/platform/python/

        - and .so files in directory specified by --dir-so.

        We avoid unnecessary compiling or running of swig by looking at file
        mtimes. We also write commands to .cmd files which allows us to force
        rebuilds if commands change.

        args:
            -f:
                Force rebuilds.

        <actions> is list of single-character actions which are processed in
        order. If <actions> is 'all', it is replaced by m0123.

            m:
                Builds libmupdf.so by running make in the mupdf/
                directory. Default is release build, but this can be changed
                using --dir-so.

            0:
                Create C++ source for C++ interface onto the fz_* API. Uses
                clang-python to parse the fz_* API.

                Generates various files including:
                    mupdf/platform/c++/
                        implementation/
                            classes.cpp
                            exceptions.cpp
                            functions.cpp
                        include/
                            classes.h
                            exceptions.h
                            functions.h

                If files already contain the generated text, they are not
                updated, so that mtimes are unchanged.

                Also removes any other .cpp or .h files from
                mupdf/platform/c++/{implmentation,include}.

            1:
                Compile and link source files created by action=0.

                Generates:
                    <dir-so>/libmupdfcpp.so

                This gives a C++ interface onto mupdf.

            2:
                Run SWIG on the C++ source built by action=0 to generate source
                for python interface onto the C++ API.

                Generates:
                    mupdf/platform/python/mupdfcpp_swig.cpp
                    mupdf/platform/python/mupdf.py

                Note that this requires action=0 to have been run previously.

            3:
                Compile and links the mupdfcpp_swig.cpp file created by
                action=2. Requires libmupdf.so to be available, e.g. built by
                the --libmupdf.so option.

                Generates:
                    mupdf/platform/python/_mupdf.so

                Along with mupdf/platform/python/mupdf.py (generated by
                action=2), this implements the mupdf python module.

    --compare-fz_usage <directory>
        Finds all fz_*() function calls in git files within <directory>, and
        compares with all the fz_*() functions that are wrapped up as class
        methods.

        Useful to see what functionality we are missing.

    --diff
        Compares generated files with those in the mupdfwrap_ref/ directory
        populated by --ref option.

    -d
    --dir-so <directory>
        Set directory containing shared libraries.

        Supported values for <directory>:
            <mupdf>/build/shared-release/ [default]
            <mupdf>/build/shared-debug/
            <mupdf>/build/shared-memento/

        We use different C++ compile flags depending on release or debug builds
        (specifically definition of NDEBUG is important because it must match
        what was used when libmupdf.so was built).

    --doc <languages>
        Generates documentation for the different APIs.

        <languages> is either 'all' or a comma-separated list of API languages:

            c
                Generate documentation for the C API with doxygen:
                    include/html/index.html
            c++
                Generate documentation for the C++ API with doxygen:
                    platform/c++/include/html/index.html
            python
                Generate documentation for the Python API using pydoc3:
                    platform/python/mupdf.html

    --ref
        Copy generated C++ files to mupdfwrap_ref/ directory for use by --diff.

    --run-py <arg> <arg> ...
        Runs command with LD_LIBRARY_PATH and PYTHONPATH set up for use with
        mupdf.py.

        Exits with same code as the command.

    --swig <swig>
        Sets the swig command to use.

        If this is version 4+, we use the <swig> -doxygen to copy
        over doxygen-style comments into mupdf.py. Otherwise we use
        '%feature("autodoc", "3");' to generate comments with type information
        for args in mupdf.py. [These two don't seem to be usable at the same
        time in swig-4.]

    --sync [-d] <destination>
        Use rsync to copy C++ source generated by action=0 to
        <destination>. Also copies syntax-coloured .html versions.

        If '-d' is specified, also sync all files generated by --doc.

        E.g. --sync julian@casper.ghostscript.com:~julian/public_html/

        As of 2020-03-09 requires patched mupdf/ checkout.

    -t:
    --test:
        Tests the python API.

Examples:

    ./scripts/mupdfwrap.py -b all -t
        Build all (release build) and test.

    ./scripts/mupdfwrap.py -d build/shared-debug -b all -t
        Build all (debug build) and test.

    ./scripts/mupdfwrap.py -b 0 --compare-fz_usage platform/gl
        Compare generated class methods with functions called by platform/gl
        code.

    python3 -m cProfile -s cumulative ./scripts/mupdfwrap.py -b 0
        Profile generation of C++ source code.


C++ wrapping:

    Details:

        All generated functions and classes are in the mupdf namespace, e.g.
        mupdf::atof() is the wrapper for fz_atof().

        We use clang-python to parse the fz header files, and generate C++
        headers and source code that gives wrappers for all fz_* functions.

        We also generate C++ classes that wrap all fz_* structs, adding
        in various constructors and methods that wrap auto-detected fz_*
        functions, plus explicitly-specified methods that wrap/use fz_*
        functions.

        More specifically, for each wrapping class:

            Copy constructors/operator=:

                If fz_keep_<name>() and fz_drop_<name>() exist, we generate
                copy constructor and operator= that use these functions.

            Constructors:

                We look for all fz_*() functions called fz_new*() that
                return a pointer to the wrapped class, and wrap these into
                constructors. If any of these constructors have duplicate
                prototypes, we cannot provide them as constructors so instead
                we provide them as static methods. This is not possible if the
                class is not copyable, in which case we include the constructor
                code but commented-out and with an explanation.

            Methods:

                We look for all fz_*() functions that take the wrapped struct
                as a first arg (ignoring any fz_context* arg), and wrap these
                into class methods. If there are duplicate prototypes, we
                comment-out all but the first.

            Other:

                There are various subleties with wrapping classes that are not
                copyable etc.

        mupdf::* functions methods generally have the same args as the fz_*
        functions that they wrap except that they don't take any fz_context*
        parameter. If the fz_* functions takes a fz_context*, one appropriate
        for the current thread is generated automatically at runtime, using
        platform/c++/implementation/internal.cpp:internal_context_get().

        All generated functions and methods convert fz_try..fz_catch exceptions
        into C++ exceptions.

        We use SWIG on this C++ API to give a python interface onto the
        mupdf::*() functions and classes.

Tools:

    Clang:

        We work with clang-6 or clang-7, but clang-6 appears to not be able
        to cope with function args that are themselves function pointers, so
        wrappers for these fz_*() functions are ommited from the generated C++
        code.

    SWIG:

        We work with swig-3 and swig-4. If swig-4 is used, we propogate
        doxygen-style comments for structures and functions into the generated
        C++ code.
'''


import glob
import inspect
import io
import os
import re
import sys
import textwrap
import time
import traceback

import jlib


log = jlib.log
log0 = jlib.log0
log1 = jlib.log1
log2 = jlib.log2
log3 = jlib.log3
log4 = jlib.log4
log5 = jlib.log5
logx = jlib.logx

# We use f-strings, so need python-3.6+.
assert sys.version_info[0] == 3 and sys.version_info[1] >= 6, (
        'We require python-3.6+')


try:
    import clang.cindex

except ModuleNotFoundError:

    # On devuan, clang-python isn't on python3's path, but python2's
    # clang-python works fine with python3, so we deviously get the path by
    # running some python 2.
    #
    clang_path = jlib.system( 'python2 -c "import clang; print clang.__path__[0]"', out='return')
    #log( 'Retrying import of clang using info from python2 {clang_path=}')

    sys.path.append( os.path.dirname( clang_path))
    import clang.cindex



class ClangInfo:
    '''
    Sets things up so we can import and use clang.

    Members:
        .libclang_so
        .resource_dir
        .include_path
        .clang_version
    '''
    def __init__( self):
        '''
        We look for different versions of clang until one works.

        Searches for libclang.so and registers with
        clang.cindex.Config.set_library_file(). This appears to be necessary
        even when clang is installed as a standard package.
        '''
        for version in 7, 6,:
            ok = self._try_init_clang( version)
            if ok:
                break
        else:
            raise Exception( 'cannot find libclang.so')

    def _try_init_clang( self, version):
        for p in os.environ.get( 'PATH').split( ':'):
            clang_bins = glob.glob( os.path.join( p, f'clang-{version}*'))
            if not clang_bins:
                continue
            clang_bins.sort()
            for clang_bin in clang_bins:
                e, clang_search_dirs = jlib.system(
                        f'{clang_bin} -print-search-dirs',
                        #verbose=log,
                        out='return',
                        raise_errors=False,
                        )
                if e:
                    log( '[could not find {clang_bin}: {e=}]')
                    return
                clang_search_dirs = clang_search_dirs.strip().split(':')
                for i in clang_search_dirs:
                    p = os.path.join( i, f'libclang-{version}.*so*')
                    p = os.path.abspath( p)
                    libclang_so = glob.glob( p)
                    if not libclang_so:
                        continue

                    # We have found libclang.so.
                    self.libclang_so = libclang_so[0]
                    log1( 'Using {self.libclang_so=}')
                    clang.cindex.Config.set_library_file( self.libclang_so)
                    self.resource_dir = jlib.system(
                            f'{clang_bin} -print-resource-dir',
                            out='return',
                            ).strip()
                    self.include_path = os.path.join( self.resource_dir, 'include')
                    self.clang_version = version
                    return True


g_clang_info = ClangInfo()



def snake_to_camel( name, initial):
    '''
    Converts foo_bar to FooBar or fooBar.
    '''
    items = name.split( '_')
    ret = ''
    for i, item in enumerate( items):
        if not item:
            item = '_'
        elif i or initial:
            item = item[0].upper() + item[1:]
        ret += item
    return ret

assert snake_to_camel( 'foo_bar', True) == 'FooBar'
assert snake_to_camel( 'foo_bar_q__a', False) == 'fooBarQ_A'


def clip( text, prefixes, suffixes=''):
    '''
    Returns <text> with prefix(s) and suffix(s) removed if present.
    '''
    if isinstance( prefixes, str):
        prefixes = prefixes,
    if isinstance( suffixes, str):
        suffixes = suffixes,
    for prefix in prefixes:
        if text.startswith( prefix):
            text = text[ len( prefix):]
            break
    for suffix in suffixes:
        if suffix and text.endswith( suffix):
            text = text[ :-len( suffix)]
            break
    return text

def fileline( cursor):
    return f'{cursor.location.file}:{cursor.location.line}'


class Rename:
    '''
    Rename functions that generate camelCase class names and lower-case
    function names with underscores.

    Using camel case in function names seems to result in gcc errors when
    compiling the code created by swig -python. e.g. in _wrap_vthrow_fn()

    mupdfcpp_swig.cpp: In function PyObject* _wrap_vthrow_fn(PyObject*, PyObject*)
    mupdfcpp_swig.cpp:88571:15: error: invalid array assignment
           arg3 = *temp;
    '''
    def function_raw( self, name):
        '''
        Name used by wrapper function when calling C function <name>.
        '''
        return f'::{name}'
    def function( self, name):
        '''
        Name of wrapper function that calls C function <name>.
        '''
        if name.startswith( 'pdf_'):
            return 'p' + name
        return f'{clip( name, "fz_")}'
    def function_call( self, name):
        '''
        Name used by class methods when calling wrapper function - we call our
        wrapper function in the mupdf:: namespace.
        '''
        return f'mupdf::{self.function(name)}'
    def class_( self, structname):
        '''
        Name of class that wraps <structname>.
        '''
        structname = clip( structname, 'struct ')
        if structname.startswith( 'fz_'):
            return snake_to_camel( clip( structname, 'fz_'), initial=True)
        elif structname.startswith( 'pdf_'):
            # Retain Pdf prefix.
            return snake_to_camel( structname, initial=True)
    def internal( self, name):
        return f'internal_{name}'
    def method( self, structname, fnname):
        if 1 and structname == 'fz_page':
            prefix = 'fz_run_page'
            if fnname.startswith( prefix):
                return f'run{fnname[len(prefix):]}'
        if structname.startswith( 'fz_'):
            return clip( fnname, "fz_")
        if structname.startswith( 'pdf_'):
            return clip( fnname, "pdf_")
        assert 0, f'unrecognised structname={structname}'

rename = Rename()

def prefix( name):
    if name.startswith( 'fz_'):
        return 'fz_'
    if name.startswith( 'pdf_'):
        return 'pdf_'
    assert 0, f'unrecognised prefix (not fz_ or pdf_) in name={name}'


# Specify extra methods to be included in generated classes.
#

class ExtraMethod:
    '''
    Defines a prototype and implementation of a custom method in a generated
    class.
    '''
    def __init__( self, return_, name_args, body, comment=None):
        '''
        return_:
            Return type as a string.
        name_args:
            A string describing name and args of the method:
                <method-name>(<args>)
        body:
            Implementation code including the enclosing '{...}'.
        comment:
            Optional comment; should include /* and */ or //.
        '''
        assert name_args
        self.return_ = return_
        self.name_args = name_args
        self.body = body
        self.comment = comment
        assert '\t' not in body


class ExtraConstructor:
    '''
    Defines a prototype and implementation of a custom method in a generated
    class.
    '''
    def __init__( self, name_args, body, comment=None):
        '''
        name_args:
            A string of the form: (<args>)
        body:
            Implementation code including the enclosing '{...}'.
        comment:
            Optional comment; should include /* and */ or //.
        '''
        self.return_ = ''
        self.name_args = name_args
        self.body = body
        self.comment = comment
        assert '\t' not in body


class ClassExtra:
    '''
    Information about extra methods to be added to an auto-generated class.
    '''
    def __init__( self,
            accessors=None,
            class_bottom='',
            class_post='',
            class_pre='',
            class_top='',
            constructor_prefixes=None,
            constructor_raw=True,
            constructor_excludes=None,
            constructors_extra=None,
            constructors_wrappers=None,
            copyable=True,
            extra_cpp='',
            iterator_next=None,
            methods_extra=None,
            method_wrappers=None,
            method_wrappers_static=None,
            opaque=False,
            pod=False,
            ):
        '''
        accessors:
            If true, we generate accessors methods for all items in the
            underlying struct.

            Defaults to True if pod is True, else False.

        class_bottom:
            Extra text at end of class definition, e.g. for member variables.

        class_post:
            Extra text after class definition, e.g. complete definition of
            iterator class.

        class_pre:
            Extra text before class definition, e.g. forward declaration of
            iterator class.

        class_top:
            Extra text at start of class definition, e.g. for enums.

        constructor_prefixes:
            Extra fz_*() function name prefixes that can be used by class
            constructors_wrappers. We find all functions whose name starts with one of
            the specified prefixes and which returns a pointer to the relevant
            fz struct.

            For each function we find, we make a constructor that takes the
            required arguments and set m_internal to what this function
            returns.

            If there is a '-' item, we omit the default 'fz_new_<type>' prefix.

        constructor_raw:
            If true, create a constructor that takes a pointer to an instance
            of the wrapped fz_ struct. If 'default', this constructor arg
            defaults to NULL.

        constructor_excludes:
            Lists of constructor functions to ignore.

        constructors_extra:
            List of ExtraConstructor's, allowing arbitrary constructors_wrappers to be
            specified.

        constructors_wrappers:
            List of fns to use as constructors_wrappers.

        copyable:
            If 'default' we allow default copy constructor to be created by C++
            compiler.

            Otherwise if true, generated wrapping class must be copyable. If
            pod is false, we generate a copy constructor by looking for a
            fz_keep_*() function; it's an error if we can't find this function.

            Otherwise if false we create a private copy constructor.

            [todo: need to check docs for interaction of pod/copyable.]

        extra_cpp:
            Extra text for .cpp file, e.g. implementation of iterator class
            methods.

        iterator_next:
            Support for iterating forwards over linked list.

            Should be (first, last).

            first:
                Name of element within the wrapped class that points to the
                first item in the linked list. We assume that this element will
                have 'next' pointers that terminate in NULL.

                If <first> is '', the container is itself the first element in
                the linked list.

            last:
                Currently unused, but could be used for reverse iteration in
                the future.

            We generate begin() and end() methods, plus a separate iterator
            class, to allow iteration over the linked list starting at
            <structname>::<first> and iterating to ->next until we reach NULL.

        methods_extra:
            List of ExtraMethod's, allowing arbitrary methods to be specified.

        method_wrappers:
            Extra fz_*() function names that should be wrapped in class
            methods.

            E.g. 'fz_foo_bar' is converted to a method called foo_bar()
            that takes same parameters as fz_foo_bar() except context and
            any pointer to struct and fz_context*. The implementation calls
            fz_foo_bar(), converting exceptions etc.

            The first arg that takes underlying fz_*_s type is omitted and
            implementation passes <this>.

        method_wrappers_static:
            Like <method_wrappers>, but generates static methods, where no args
            are replaced by <this>.

        opaque:
            If true, we generate a wrapper even if there's no definition
            available for the struct, i.e. it's only available as a forward
            declaration.

        pod:
            If True, underlying class is POD and m_internal is an instance of
            the underlying class instead of a pointer to it.

            In addition if 'inline', there is no m_internal; instead, each
            member of the underlying class is placed in the wrapping class.

        '''
        if accessors is None and pod is True:
            accessors = True
        self.accessors = accessors
        self.class_bottom = class_bottom
        self.class_post = class_post
        self.class_pre = class_pre
        self.class_top = class_top
        self.constructor_excludes = constructor_excludes or []
        self.constructor_prefixes = constructor_prefixes or []
        self.constructor_raw = constructor_raw
        self.constructors_extra = constructors_extra or []
        self.constructors_wrappers = constructors_wrappers or []
        self.copyable = copyable
        self.extra_cpp = extra_cpp
        self.iterator_next = iterator_next
        self.methods_extra = methods_extra or []
        self.method_wrappers = method_wrappers or []
        self.method_wrappers_static = method_wrappers_static or []
        self.opaque = opaque
        self.pod = pod

        assert self.pod in (False, True, 'inline'), f'{self.pod}'

        assert isinstance( self.constructor_prefixes, list)
        for i in self.constructor_prefixes:
            assert isinstance( i, str)
        assert isinstance( self.method_wrappers, list)
        for i in self.method_wrappers:
            assert isinstance( i, str)
        assert isinstance( self.method_wrappers_static, list)
        assert isinstance( self.methods_extra, list)
        for i in self.methods_extra:
            assert isinstance( i, ExtraMethod)
        assert isinstance( self.constructors_extra, list)
        for i in self.constructors_extra:
            assert isinstance( i, ExtraConstructor)


class ClassExtras:
    '''
    Information about the various extra methods to be added to auto-generated
    classes.
    '''
    def __init__( self, **namevalues):
        '''
        namevalues:
            Named args mapping from struct name (e.g. fz_document) to a
            ClassExtra.
        '''
        self.items = dict()
        for name, value in namevalues.items():
            self.items[ name] = value
    def get( self, name):
        '''
        If <name> is in omit_class_names0, returns None.

        Otherwise searches for <name>; if found, returns ClassExtra instance,
        else empty ClassExtra instance.
        '''
        name = clip( name, 'struct ')
        if name in omit_class_names0:
            return
        if not name.startswith( ('fz_', 'pdf_')):
            return
        return self.items.get( name, ClassExtra())
    def get_or_none( self, name):
        return self.items.get( name)


# These functions are known to return a pointer to a fz_* struct that must not
# be dropped.
#
# This matters if we wrap in a class method, because this will return class
# wrapper for the struct, whose destructor will call fz_drop_*(). In this case,
# we need to call fz_keep_*() before returning the class wrapper.
#
functions_that_return_non_kept = [
        'fz_default_cmyk',
        'fz_default_rgb',
        'fz_default_cmyk',
        'fz_default_output_intent',
        'fz_document_output_intent',
        ]


classextras = ClassExtras(

        fz_aa_context = ClassExtra(
                pod='inline',
                ),

        fz_band_writer = ClassExtra(
                class_top = '''
                    enum Cm
                    {
                        MONO,
                        COLOR,
                    };
                    enum P
                    {
                        PNG,
                        PNM,
                        PAM,
                        PBM,
                        PKM,
                        PS,
                        PSD,
                    };
                    ''',
                constructors_extra = [
                    ExtraConstructor(
                        f'({rename.class_("fz_output")}& out, Cm cm, const {rename.class_("fz_pcl_options")}& options)',
                        f'''
                        {{
                            fz_output*              out2 = out.m_internal;
                            const fz_pcl_options*   options2 = options.m_internal;
                            if (0)  {{}}
                            else if (cm == MONO)    m_internal = {rename.function_call('fz_new_mono_pcl_band_writer' )}( out2, options2);
                            else if (cm == COLOR)   m_internal = {rename.function_call('fz_new_color_pcl_band_writer')}( out2, options2);
                            else throw std::runtime_error( "Unrecognised fz_band_writer_s Cm type");
                        }}
                        ''',
                        comment = f'/* Calls fz_new_mono_pcl_band_writer() or fz_new_color_pcl_band_writer(). */',
                        ),
                    ExtraConstructor(
                        f'({rename.class_("fz_output")}& out, P p)',
                        f'''
                        {{
                            fz_output*              out2 = out.m_internal;
                            if (0)  {{}}
                            else if (p == PNG)  m_internal = {rename.function_call('fz_new_png_band_writer')}( out2);
                            else if (p == PNM)  m_internal = {rename.function_call('fz_new_pnm_band_writer')}( out2);
                            else if (p == PAM)  m_internal = {rename.function_call('fz_new_pam_band_writer')}( out2);
                            else if (p == PBM)  m_internal = {rename.function_call('fz_new_pbm_band_writer')}( out2);
                            else if (p == PKM)  m_internal = {rename.function_call('fz_new_pkm_band_writer')}( out2);
                            else if (p == PS)   m_internal = {rename.function_call('fz_new_ps_band_writer' )}( out2);
                            else if (p == PSD)  m_internal = {rename.function_call('fz_new_psd_band_writer')}( out2);
                            else throw std::runtime_error( "Unrecognised fz_band_writer_s P type");
                        }}
                        ''',
                        comment = f'/* Calls fz_new_p*_band_writer(). */',
                        ),
                    ExtraConstructor(
                        f'({rename.class_("fz_output")}& out, Cm cm, const {rename.class_("fz_pwg_options")}& options)',
                        f'''
                        {{
                            fz_output*              out2 = out.m_internal;
                            const fz_pwg_options*   options2 = &options.m_internal;
                            if (0)  {{}}
                            else if (cm == MONO)    m_internal = {rename.function_call('fz_new_mono_pwg_band_writer' )}( out2, options2);
                            else if (cm == COLOR)   m_internal = {rename.function_call('fz_new_pwg_band_writer')}( out2, options2);
                            else throw std::runtime_error( "Unrecognised fz_band_writer_s Cm type");
                        }}
                        ''',
                        comment = f'/* Calls fz_new_mono_pwg_band_writer() or fz_new_pwg_band_writer(). */',
                        ),
                    ],
                copyable = False,
                ),

        fz_bitmap = ClassExtra(
                accessors = True,
                ),

        fz_buffer = ClassExtra(
                constructors_wrappers = [
                    'fz_read_file',
                    ],
                ),

        fz_color_params = ClassExtra(
                pod='inline',
                ),

        fz_colorspace = ClassExtra(
                constructors_extra = [
                    ExtraConstructor(
                        '(Fixed fixed)',
                        f'''
                        {{
                            if (0) {{}}
                            else if ( fixed == Fixed_GRAY)  m_internal = {rename.function_call( 'fz_device_gray')}();
                            else if ( fixed == Fixed_RGB)   m_internal = {rename.function_call( 'fz_device_rgb' )}();
                            else if ( fixed == Fixed_BGR)   m_internal = {rename.function_call( 'fz_device_bgr' )}();
                            else if ( fixed == Fixed_CMYK)  m_internal = {rename.function_call( 'fz_device_cmyk')}();
                            else if ( fixed == Fixed_LAB)   m_internal = {rename.function_call( 'fz_device_lab' )}();
                            else {{
                                std::string message = "Unrecognised fixed colorspace id";
                                throw ErrorGeneric(message.c_str());
                            }}
                            {rename.function_call('fz_keep_colorspace')}(m_internal);
                        }}
                        ''',
                        ),
                        ExtraConstructor(
                        '()',
                        '''
                        : m_internal( NULL)
                        {
                        }
                        ''',
                        comment = '/* Sets m_internal = NULL. */',
                        ),
                    ],
                constructor_raw=1,
                class_top = '''
                        enum Fixed
                        {
                            Fixed_GRAY,
                            Fixed_RGB,
                            Fixed_BGR,
                            Fixed_CMYK,
                            Fixed_LAB,
                        };
                        ''',
                ),

        fz_cookie = ClassExtra(
                constructors_extra = [
                    ExtraConstructor( '()',
                    '''
                    {
                        this->m_internal.abort = 0;
                        this->m_internal.progress = 0;
                        this->m_internal.progress_max = (size_t) -1;
                        this->m_internal.errors = 0;
                        this->m_internal.incomplete = 0;
                    }
                    ''',
                    comment = '/* Sets all fields to default values. */',
                    ),
                    ],
                constructor_raw = False,
                methods_extra = [
                    ExtraMethod( 'void',    'set_abort()',                  '{ m_internal.abort = 1; }\n'),

                    # do we need these? - already provided by accessors.
                    ExtraMethod( 'int',     'get_progress()',               '{ return m_internal.progress; }\n'),
                    ExtraMethod( 'size_t',  'get_progress_max()',           '{ return m_internal.progress_max; }\n'),
                    ExtraMethod( 'int',     'get_errors()',                 '{ return m_internal.errors; }\n'),
                    ExtraMethod( 'int',     'get_incomplete()',             '{ return m_internal.incomplete; }\n'),

                    ExtraMethod( 'void',    'increment_errors(int delta)',  '{ m_internal.errors += delta; }\n'),
                ],
                pod = True,
                # I think other code asyncronously writes to our fields, so we
                # are not be copyable. todo: maybe tie us to all objects to
                # which we have been associated?
                #
                copyable=False,
                ),

        fz_device = ClassExtra(
                constructor_raw = True,
                method_wrappers_static = [
                        ],
                constructors_extra = [
                    ExtraConstructor( '()',
                        '''
                        : m_internal( NULL)
                        {
                        }
                        ''',
                        comment = '/* Sets m_internal = NULL. */',
                        ),
                    ]
                ),

        fz_display_list = ClassExtra(
                ),

        fz_document = ClassExtra(
                constructor_excludes = [
                    'fz_new_xhtml_document_from_document',
                    ],
                constructor_prefixes = [
                    'fz_open_accelerated_document',
                    'fz_open_document',
                    ],
                method_wrappers_static = [
                    'fz_new_xhtml_document_from_document',
                    ],
                ),

        fz_document_writer = ClassExtra(
                class_top = '''
                    enum Type
                    {
                        Type_CBZ,
                        Type_PAM_PIXMAP,
                        Type_PBM_PIXMAP,
                        Type_PCL,
                        Type_PCLM,
                        Type_PGM_PIXMAP,
                        Type_PKM_PIXMAP,
                        Type_PNG_PIXMAP,
                        Type_PNM_PIXMAP,
                        Type_PPM_PIXMAP,
                        Type_PS,
                        Type_PWG,
                        Type_SVG,
                    };
                    enum OutputType
                    {
                        OutputType_CBZ,
                        OutputType_PCL,
                        OutputType_PCLM,
                        OutputType_PS,
                        OutputType_PWG,
                    };
                ''',
                constructor_excludes = [
                    'fz_new_cbz_writer',
                    'fz_new_pam_pixmap_writer',
                    'fz_new_pbm_pixmap_writer',
                    'fz_new_pcl_writer',
                    'fz_new_pclm_writer',
                    'fz_new_pgm_pixmap_writer',
                    'fz_new_pkm_pixmap_writer',
                    'fz_new_png_pixmap_writer',
                    'fz_new_pnm_pixmap_writer',
                    'fz_new_ppm_pixmap_writer',
                    'fz_new_ps_writer',
                    'fz_new_pwg_writer',
                    'fz_new_svg_writer',

                    'fz_new_cbz_writer_with_output',
                    'fz_new_pcl_writer_with_output',
                    'fz_new_pclm_writer_with_output',
                    'fz_new_ps_writer_with_output',
                    'fz_new_pwg_writer_with_output',
                    ],

                copyable=False,
                methods_extra = [
                    ExtraMethod( 'Device', 'begin_page(Rect& mediabox)',
                        f'''
                        {{
                            /* fz_begin_page() doesn't transfer ownership, so
                            we have to call fz_keep_device() before creating
                            the Device instance. */
                            fz_device* dev = {rename.function_call('fz_begin_page')}(m_internal, *(fz_rect*) &mediabox.x0);
                            dev = {rename.function_call('fz_keep_device')}(dev);
                            return Device(dev);
                        }}
                        '''),
                        ],
                constructors_extra = [
                    ExtraConstructor(
                        '(const char *path, const char *options, Type type)',
                        f'''
                        {{
                            if (0) {{}}
                            else if (type == Type_CBZ)          m_internal = {rename.function_call( 'fz_new_cbz_writer')}(path, options);
                            else if (type == Type_PAM_PIXMAP)   m_internal = {rename.function_call( 'fz_new_pam_pixmap_writer')}(path, options);
                            else if (type == Type_PBM_PIXMAP)   m_internal = {rename.function_call( 'fz_new_pbm_pixmap_writer')}(path, options);
                            else if (type == Type_PCL)          m_internal = {rename.function_call( 'fz_new_pcl_writer')}(path, options);
                            else if (type == Type_PCLM)         m_internal = {rename.function_call( 'fz_new_pclm_writer')}(path, options);
                            else if (type == Type_PGM_PIXMAP)   m_internal = {rename.function_call( 'fz_new_pgm_pixmap_writer')}(path, options);
                            else if (type == Type_PKM_PIXMAP)   m_internal = {rename.function_call( 'fz_new_pkm_pixmap_writer')}(path, options);
                            else if (type == Type_PNG_PIXMAP)   m_internal = {rename.function_call( 'fz_new_png_pixmap_writer')}(path, options);
                            else if (type == Type_PNM_PIXMAP)   m_internal = {rename.function_call( 'fz_new_pnm_pixmap_writer')}(path, options);
                            else if (type == Type_PPM_PIXMAP)   m_internal = {rename.function_call( 'fz_new_ppm_pixmap_writer')}(path, options);
                            else if (type == Type_PS)           m_internal = {rename.function_call( 'fz_new_ps_writer')}(path, options);
                            else if (type == Type_PWG)          m_internal = {rename.function_call( 'fz_new_pwg_writer')}(path, options);
                            else if (type == Type_SVG)          m_internal = {rename.function_call( 'fz_new_svg_writer')}(path, options);
                            else throw ErrorAbort( "Unrecognised Type value");
                        }}
                        ''',
                        comment = '/* Calls one of: fz_new_cbz_writer, fz_new_pam_pixmap_writer, fz_new_pbm_pixmap_writer, fz_new_pcl_writer, fz_new_pclm_writer, fz_new_pgm_pixmap_writer, fz_new_pkm_pixmap_writer, fz_new_png_pixmap_writer, fz_new_pnm_pixmap_writer, fz_new_ppm_pixmap_writer, fz_new_ps_writer, fz_new_pwg_writer, fz_new_svg_writer, */',
                        ),
                    ExtraConstructor(
                        '(const Output& out, const char *options, OutputType output_type)',
                        f'''
                        {{
                            if (0) {{}}
                            else if (output_type == OutputType_CBZ)     m_internal = {rename.function_call( 'fz_new_cbz_writer_with_output')}(out.m_internal, options);
                            else if (output_type == OutputType_PCL)     m_internal = {rename.function_call( 'fz_new_pcl_writer_with_output')}(out.m_internal, options);
                            else if (output_type == OutputType_PCLM)    m_internal = {rename.function_call( 'fz_new_pclm_writer_with_output')}(out.m_internal, options);
                            else if (output_type == OutputType_PS)      m_internal = {rename.function_call( 'fz_new_ps_writer_with_output')}(out.m_internal, options);
                            else if (output_type == OutputType_PWG)     m_internal = {rename.function_call( 'fz_new_pwg_writer_with_output')}(out.m_internal, options);
                            else throw ErrorAbort( "Unrecognised OutputType value");
                        }}
                        ''',
                        comment = '/* Calls one of: fz_new_cbz_writer_with_output, fz_new_pcl_writer_with_output, fz_new_pclm_writer_with_output, fz_new_ps_writer_with_output, fz_new_pwg_writer_with_output. */',
                        ),
                    ],

                ),

        fz_draw_options = ClassExtra(
                constructors_wrappers = [
                    'fz_parse_draw_options',
                    ],
                copyable=False,
                pod='inline',
                ),

        fz_font = ClassExtra(
                ),

        fz_glyph = ClassExtra(
                class_top =
                    '''
                    enum Bpp
                    {
                        Bpp_1,
                        Bpp_8,
                    };
                    ''',
                ),

        fz_halftone = ClassExtra(
                constructor_raw = 'default',
                ),

        fz_irect = ClassExtra(
                constructor_prefixes = [
                    'fz_irect_from_rect',
                    'fz_make_irect',
                    ],
                pod='inline',
                constructor_raw = True,
                ),

        fz_link = ClassExtra(
                iterator_next = ('', ''),
                constructor_raw = True,
                ),

        fz_location = ClassExtra(
                constructor_prefixes = [
                    'fz_make_location',
                    ],
                pod='inline',
                constructor_raw = True,
                ),

        fz_matrix = ClassExtra(
                constructor_prefixes = [
                    'fz_make_matrix',
                    ],
                method_wrappers_static = [
                    'fz_concat',
                    'fz_scale',
                    'fz_shear',
                    'fz_rotate',
                    'fz_translate',
                    'fz_transform_page',
                    ],
                constructors_extra = [
                    ExtraConstructor( '()',
                        '''
                        : a(1), b(0), c(0), d(1), e(0), f(0)
                        {
                        }
                        ''',
                        comment = '/* Constructs identity matrix (like fz_identity). */'),
                ],
                pod='inline',
                constructor_raw = True,
                ),

        fz_outline = ClassExtra(
                # We add various methods to give depth-first iteration of outlines.
                #
                constructor_raw = False,
                constructor_prefixes = [
                    'fz_load_outline',
                    ],
                constructors_extra = [
                    ExtraConstructor( '(struct fz_outline* internal)',
                    f'''
                    : m_internal( {rename.function_call("fz_keep_outline")}(internal))
                    {{
                    }}
                    '''
                    ),
                    ],
                methods_extra = [
                    ExtraMethod( 'OutlineIterator', 'begin()',
                    '''
                    {
                        return OutlineIterator(*this);
                    }
                    ''',
                    ),
                    ExtraMethod( 'OutlineIterator', 'end()',
                    '''
                    {
                        return OutlineIterator();
                    }
                    '''),
                    ],
                class_bottom = 'typedef OutlineIterator iterator;\n',
                class_pre =
                    '''
                    struct OutlineIterator;
                    ''',
                class_post =
                    '''
                    struct OutlineIterator
                    {
                        OutlineIterator();
                        OutlineIterator(const Outline& item);
                        OutlineIterator& operator++();
                        bool operator==(const OutlineIterator& rhs);
                        bool operator!=(const OutlineIterator& rhs);
                        Outline& operator*();
                        Outline* operator->();
                        private:
                        Outline m_item;
                        std::vector<fz_outline*> m_up;
                    };
                    ''',
                extra_cpp =
                    '''
                    OutlineIterator::OutlineIterator(const Outline& item)
                    : m_item(item)
                    {
                    }
                    OutlineIterator::OutlineIterator()
                    : m_item(NULL)
                    {
                    }
                    OutlineIterator& OutlineIterator::operator++()
                    {
                        if (m_item.m_internal->down) {
                            m_up.push_back(m_item.m_internal);
                            m_item = Outline(m_item.m_internal->down);
                        }
                        else if (m_item.m_internal->next) {
                            m_item = Outline(m_item.m_internal->next);
                        }
                        else {
                            /* Go up and across in the tree. */
                            for(;;) {
                                if (m_up.empty()) {
                                    m_item = Outline(NULL);
                                    break;
                                }
                                fz_outline* p = m_up.back();
                                m_up.pop_back();
                                if (p->next) {
                                    m_item = Outline(p->next);
                                    break;
                                }
                            }
                        }
                        return *this;
                    }
                    bool OutlineIterator::operator==(const OutlineIterator& rhs)
                    {
                        bool ret = m_item.m_internal == rhs.m_item.m_internal;
                        return ret;
                    }
                    bool OutlineIterator::operator!=(const OutlineIterator& rhs)
                    {
                        return m_item.m_internal != rhs.m_item.m_internal;
                    }
                    Outline& OutlineIterator::operator*()
                    {
                        return m_item;
                    }
                    Outline* OutlineIterator::operator->()
                    {
                        return &m_item;
                    }

                    void test(Outline& item)
                    {
                        for( OutlineIterator it = item.begin(); it != item.end(); ++it) {
                            (void) *it;
                        }
                        for (auto i: item) {
                            (void) i;
                        }
                    }

                    ''',
                accessors=True,
                ),

        fz_output = ClassExtra(
                constructor_excludes = [
                    # These all have the same prototype, so are used by
                    # constructors_extra below.
                    'fz_new_asciihex_output',
                    'fz_new_ascii85_output',
                    'fz_new_rle_output',
                    ],
                constructors_extra = [
                    ExtraConstructor( '(Fixed out)',
                        f'''
                        {{
                            if (0)  {{}}
                            else if (out == Fixed_STDOUT) {{
                                m_internal = {rename.function_call('fz_stdout')}();
                            }}
                            else if (out == Fixed_STDERR) {{
                                m_internal = {rename.function_call('fz_stderr')}();
                            }}
                            else {{
                                throw ErrorAbort("Unrecognised Fixed value");
                            }}
                        }}
                        '''
                        # Note that it's ok to call fz_drop_output() on fz_stdout and fz_stderr.
                        ),
                    ExtraConstructor(
                        f'(const {rename.class_("fz_output")}& chain, Filter filter)',
                        f'''
                        {{
                            if (0)  {{}}
                            else if (filter == Filter_HEX) {{
                                m_internal = {rename.function_call('fz_new_asciihex_output')}(chain.m_internal);
                            }}
                            else if (filter == Filter_85) {{
                                m_internal = {rename.function_call('fz_new_ascii85_output')}(chain.m_internal);
                            }}
                            else if (filter == Filter_RLE) {{
                                m_internal = {rename.function_call('fz_new_rle_output')}(chain.m_internal);
                            }}
                            else {{
                                throw ErrorAbort("Unrecognised Filter value");
                            }}
                        }}
                        ''',
                        comment = '/* Calls one of: fz_new_asciihex_output(), fz_new_ascii85_output(), fz_new_rle_output(). */',
                        ),
                    ],
                class_top = '''
                    enum Fixed
                    {
                        Fixed_STDOUT=1,
                        Fixed_STDERR=2,
                    };
                    enum Filter
                    {
                        Filter_HEX,
                        Filter_85,
                        Filter_RLE,
                    };
                    '''
                    ,
                copyable=False, # No fz_keep_output() fn?
                ),

        fz_page = ClassExtra(
                constructor_prefixes = [
                    'fz_load_page',
                    'fz_load_chapter_page',
                    ],
                constructor_raw = True,
                ),

        fz_pcl_options = ClassExtra(
                constructors_wrappers = [
                    'fz_parse_pcl_options',
                    ],
                copyable=False,
                ),

        fz_pclm_options = ClassExtra(
                constructor_prefixes = [
                    'fz_parse_pclm_options',
                    ],
                copyable=False,
                constructors_extra = [
                    ExtraConstructor( '(const char *args)',
                        f'''
                        {{
                            {rename.function_call('fz_parse_pclm_options')}(m_internal, args);
                        }}
                        '''
                        )
                    ],
                ),

        fz_pixmap = ClassExtra(
                methods_extra = [
                    ExtraMethod( 'std::string', 'md5_pixmap()',
                        f'''
                        {{
                            unsigned char   digest[16];
                            {rename.function_call( 'fz_md5_pixmap')}( m_internal, digest);
                            return std::string( (char*) digest);
                        }}
                        ''')
                    ],
                constructor_raw = True,
                accessors = True,
                ),

        fz_point = ClassExtra(
                method_wrappers_static = [
                    'fz_transform_point',
                    'fz_transform_point_xy',
                    'fz_transform_vector',

                    ],
                constructors_extra = [
                    ExtraConstructor( '(float x, float y)',
                        '''
                        : x(x), y(y)
                        {
                        }
                        '''),
                        ],
                methods_extra = [
                    ExtraMethod(
                        f'{rename.class_("fz_point")}&',
                        f'transform(const {rename.class_("fz_matrix")}& m)',
                        '''
                        {
                            double  old_x = x;
                            x = old_x * m.a + y * m.c + m.e;
                            y = old_x * m.b + y * m.d + m.f;
                            return *this;
                        }
                        '''),
                ],
                pod='inline',
                constructor_raw = True,
                ),

        fz_pwg_options = ClassExtra(
                pod=True,
                ),

        fz_quad = ClassExtra(
                constructor_prefixes = [
                    'fz_transform_quad',
                    ],
                pod='inline',
                constructor_raw = True,
                ),

        fz_rect = ClassExtra(
                constructor_prefixes = [
                    'fz_transform_rect',
                    'fz_bound_display_list',
                    'fz_rect_from_irect',
                    'fz_rect_from_quad',
                    ],
                method_wrappers_static = [
                    'fz_intersect_rect',
                    'fz_union_rect',
                    ],
                constructors_extra = [
                    ExtraConstructor(
                        '(double x0, double y0, double x1, double y1)',
                        '''
                        :
                        x0(x0),
                        x1(x1),
                        y0(y0),
                        y1(y1)
                        {
                        }
                        '''),
                    ExtraConstructor(
                        f'(const {rename.class_("fz_rect")}& rhs)',
                        '''
                        :
                        x0(rhs.x0),
                        x1(rhs.x1),
                        y0(rhs.y0),
                        y1(rhs.y1)
                        {
                        }
                        '''),
                    ExtraConstructor( '(Fixed fixed)',
                        f'''
                        {{
                            if (0)  {{}}
                            else if (fixed == Fixed_UNIT)       *this->internal() = {rename.function_raw('fz_unit_rect')};
                            else if (fixed == Fixed_EMPTY)      *this->internal() = {rename.function_raw('fz_empty_rect')};
                            else if (fixed == Fixed_INFINITE)   *this->internal() = {rename.function_raw('fz_infinite_rect')};
                            throw ErrorAbort( "Unrecognised From value");
                        }}
                        '''),
                        ],
                methods_extra = [
                    ExtraMethod(
                        'void',
                        f'transform(const {rename.class_("fz_matrix")}& m)',
                        f'''
                        {{
                            *(fz_rect*) &this->x0 = {rename.function_raw('fz_transform_rect')}(*(fz_rect*) &this->x0, *(fz_matrix*) &m.a);
                        }}
                        '''),
                    ExtraMethod( 'bool', 'contains(double x, double y)',
                        '''
                        {
                            if (is_empty()) {
                                return false;
                            }
                            return true
                                    && x >= x0
                                    && x < x1
                                    && y >= y0
                                    && y < y1
                                    ;
                        }
                        '''),
                    ExtraMethod( 'bool', f'contains({rename.class_("fz_rect")}& rhs)',
                        f'''
                        {{
                            return {rename.function_raw('fz_contains_rect')}(*(fz_rect*) &x0, *(fz_rect*) &rhs.x0);
                        }}
                        '''),
                    ExtraMethod( 'bool', 'is_empty()',
                        f'''
                        {{
                            return {rename.function_raw('fz_is_empty_rect')}(*(fz_rect*) &x0);
                        }}
                        '''),
                    ExtraMethod( 'void', f'union_({rename.class_("fz_rect")}& rhs)',
                        f'''
                        {{
                            *(fz_rect*) &x0 = {rename.function_raw('fz_union_rect')}(*(fz_rect*) &x0, *(fz_rect*) &rhs.x0);
                        }}
                        '''),
                    ],
                pod='inline',
                constructor_raw = True,
                copyable = True,
                class_top = '''
                    enum Fixed
                    {
                        Fixed_UNIT,
                        Fixed_EMPTY,
                        Fixed_INFINITE,
                    };
                    ''',
                ),

        fz_separations = ClassExtra(
                constructor_raw = True,
                opaque = True,
                ),

        fz_stext_block = ClassExtra(
                iterator_next = ('u.t.first_line', 'u.t.last_line'),
                copyable='default', # needs to be copyable to allow iterator.
                ),

        fz_stext_char = ClassExtra(
                copyable='default',
                ),

        fz_stext_line = ClassExtra(
                iterator_next = ('first_char', 'last_char'),
                copyable='default',
                constructor_raw=True,
                ),

        fz_stext_options = ClassExtra(
                constructors_extra = [
                    ExtraConstructor( '(int flags)',
                        '''
                        : flags( flags)
                        {
                        }
                        '''),
                    ],
                pod='inline',
                ),

        fz_stext_page = ClassExtra(
                methods_extra = [
                    ExtraMethod( 'std::string', 'copy_selection(Point& a, Point& b, int crlf)',
                        f'''
                        {{
                            char* text = {rename.function_call('fz_copy_selection')}(m_internal, *(fz_point *) &a.x, *(fz_point *) &b.x, crlf);
                            std::string ret(text);
                            {rename.function_call('fz_free')}(text);
                            return ret;
                        }}
                        ''',
                        comment = f'/* Wrapper for fz_copy_selection(). */',
                        ),
                    ],
                iterator_next = ('first_block', 'last_block'),
                copyable=False,
                constructor_raw = True,
                ),

        fz_text_span = ClassExtra(
                copyable=False,
                ),

        fz_stream = ClassExtra(
                constructor_prefixes = [
                    'fz_open_file',
                    'fz_open_memory',
                    ],
                constructors_extra = [
                    ExtraConstructor( '(const std::string& filename)',
                    f'''
                    : m_internal({rename.function_call('fz_open_file')}(filename.c_str()))
                    {{
                    }}
                    '''
                    )
                    ],
                ),

        fz_transition = ClassExtra(
                pod='inline',
                constructor_raw = True,
                ),

        pdf_document = ClassExtra(
                constructor_prefixes = [
                    'pdf_open_document',
                    ],
                ),

        pdf_write_options = ClassExtra(
                constructors_extra = [
                    ExtraConstructor( '()',
                        f'''
                        {{
                            /* Use memcpy() otherwise we get 'invalid array assignment' errors. */
                            memcpy(this->internal(), &pdf_default_write_options, sizeof(*this->internal()));
                        }}
                        ''',
                        comment = '/* Default constructor, makes copy of pdf_default_write_options. */'
                        ),
                    ExtraConstructor(
                        f'(const {rename.class_("pdf_write_options")}& rhs)',
                        f'''
                        {{
                            /* Use memcpy() otherwise we get 'invalid array assignment' errors. */
                            *this = rhs;
                        }}
                        ''',
                        ),
                    ],
                    methods_extra = [
                        ExtraMethod(
                            f'{rename.class_("pdf_write_options")}&',
                            f'operator=(const {rename.class_("pdf_write_options")}& rhs)',
                            f'''
                            {{
                                memcpy(this->internal(), rhs.internal(), sizeof(*this->internal()));
                                return *this;
                            }}
                            ''',
                            ),
                    ],
                pod = 'inline',
                copyable = 'default',
                )
        )

def get_fz_extras( fzname):
    '''
    Finds ClassExtra for <fzname>, coping if <fzname> starts with 'const ' or
    'struct '.
    '''
    fzname = clip( fzname, 'const ')
    fzname = clip( fzname, 'struct ')
    ce = classextras.get( fzname)
    return ce

def get_field0( type_):
    '''
    Returns cursor for first field in <type_> or None if <type_> has no fields.
    '''
    assert isinstance( type_, clang.cindex.Type)
    type_ = type_.get_canonical()
    for field in type_.get_fields():
        return field

get_base_type_cache = dict()
def get_base_type( type_):
    '''
    Follows pointer to get ultimate type.
    '''
    # Caching reduces time from to 0.24s to 0.1s.
    key = type_.spelling
    ret = get_base_type_cache.get( key)
    if ret is None:
        while 1:
            type_ = type_.get_canonical()
            if type_.kind != clang.cindex.TypeKind.POINTER:
                break
            type_ = type_.get_pointee()
        ret = type_
        get_base_type_cache[ key] = ret

    return ret

def get_base_typename( type_):
    '''
    Follows pointer to get ultimate type, and returns its name, with any
    leading 'struct ' or 'const ' removed.
    '''
    type_ = get_base_type( type_)
    ret = type_.spelling
    ret = clip( ret, 'const ')
    ret = clip( ret, 'struct ')
    return ret

def is_double_pointer( type_):
    '''
    Returns true if <type_> is double pointer.
    '''
    type_ = type_.get_canonical()
    if type_.kind == clang.cindex.TypeKind.POINTER:
        type_ = type_.get_pointee().get_canonical()
        if type_.kind == clang.cindex.TypeKind.POINTER:
            return True


def write_call_arg(
        cursor,
        name,
        separator,
        alt,
        out_param,
        classname,
        have_used_this,
        out_cpp,
        verbose=False,
        ):
    '''
    Write an arg of a function call, translating between raw and wrapping
    classes as appropriate.

    If the required type is a fz_ struct that we wrap, we assume that the
    <name> is a reference to an instance of the wrapping class. If the wrapping
    class is the same as <classname>, we use 'this->' instead of <name>. We
    also generate slightly different code depending on whether the wrapping
    class is pod or inline pod.

    (cursor, name, separator, alt) should be as if from get_args().

    cursor:
        Clang cursor for the argument in the raw fz_ function.
    name:
        Name of available variable.
    separator:
        .
    alt:
        Cursor for wrapped class or None.
    classname:
        Name of wrapping class available as 'this'.
    have_used_this:
        If true, we never use 'this->...'.
    out_cpp:
        .

    Returns True if we have used 'this->...', else return <have_used_this>.
    '''
    assert isinstance( cursor, clang.cindex.Cursor)
    out_cpp.write( separator)
    if not alt:
        # Arg is a normal type; no conversion necessary.
        out_cpp.write( name)
        return have_used_this

    if verbose:
        log( '{cursor.spelling=} {name=} {alt.spelling=} {classname=}')
    type_ = cursor.type.get_canonical()
    ptr = '*'
    if type_.kind == clang.cindex.TypeKind.POINTER:
        type_ = type_.get_pointee().get_canonical()
        ptr = ''
    extras = get_fz_extras( type_.spelling)
    assert extras
    if verbose:
        log( 'param is fz: {type_.spelling=} {extras2.pod=}')
    if extras.pod == 'inline':
        # We use the address of the first class member, casting it to a pointer
        # to the wrapped type. Not sure this is guaranteed safe, but should
        # work in practise.
        name_ = f'{name}.'
        if not have_used_this and rename.class_(alt.type.spelling) == classname:
            have_used_this = True
            name_ = 'this->'
        field0 = get_field0(type_).spelling
        out_cpp.write( f'{ptr}({cursor.type.spelling}{ptr}) &{name_}{field0}')
    else:
        if verbose:
            log( '{cursor=} {name=} {classname=} {extras2.pod=}')
        if extras.pod and cursor.type.get_canonical().kind == clang.cindex.TypeKind.POINTER:
            out_cpp.write( '&')
        elif out_param:
            out_cpp.write( '&')
        if not have_used_this and rename.class_(alt.type.spelling) == classname:
            have_used_this = True
            out_cpp.write( 'this->')
        else:
            out_cpp.write( f'{name}.')
        out_cpp.write( 'm_internal')

    return have_used_this


omit_fns = [
        'fz_open_file_w',
        'fz_set_stderr',
        'fz_set_stdout',
        'fz_colorspace_name_process_colorants', # Not implemented in mupdf.so?
        'fz_clone_context_internal',            # Not implemented in mupdf?
        'fz_arc4_final',
        'fz_assert_lock_held',      # Is a macro if NDEBUG defined.
        'fz_assert_lock_not_held',  # Is a macro if NDEBUG defined.
        'fz_lock_debug_lock',       # Is a macro if NDEBUG defined.
        'fz_lock_debug_unlock',     # Is a macro if NDEBUG defined.
        ]

omit_methods = [
        'fz_encode_character_with_fallback',    # Has 'fz_font **out_font' arg.
        'fz_new_draw_device_with_options',      # Has 'fz_pixmap **pixmap' arg.
        ]

# Be able to exclude some structs from being wrapped.
omit_class_names0 = []
omit_class_names = omit_class_names0[:]

def omit_class( fzname):
    '''
    Returns true if we ommit <fzname> *and* we haven't been called for <fzname>
    before.
    '''
    try:
        omit_class_names.remove( fzname)
    except Exception:
        return False
    return True

def get_value( item, name):
    '''
    Enhanced wrapper for getattr().

    We call ourselves recursively if name contains one or more '.'. If name
    ends with (), makes fn call to get value.
    '''
    if not name:
        return item
    dot = name.find( '.')
    if dot >= 0:
        item_sub = get_value( item, name[:dot])
        return get_value( item_sub, name[dot+1:])
    if name.endswith('()'):
        value = getattr( item, name[:-2])
        assert callable(value)
        return value()
    return getattr( item, name)

def get_list( item, *names):
    '''
    Uses get_value() to find values of specified fields in <item>.

    Returns list of (name,value) pairs.
    '''
    ret = []
    for name in names:
        value = get_value( item, name)
        ret.append((name, value))
    return ret

def get_text( item, prefix, sep, *names):
    '''
    Returns text describing <names> elements of <item>.
    '''
    ret = []
    for name, value in get_list( item, *names):
        ret.append( f'{name}={value}')
    return prefix + sep.join( ret)

class Clang6FnArgsBug( Exception):
    def __init__( self, text):
        Exception.__init__( self, f'clang-6 unable to walk args for fn type. {text}')

def declaration_text( type_, name, nest=0, name_is_simple=True, arg_names=False, verbose=False):
    '''
    Returns text for C++ declaration of <type_> called <name>.

    type:
        a clang.cindex.Type.
    name:
        name of type; can be empty.
    nest:
        for internal diagnostics.
    name_is_simple:
        true iff <name> is an identifier.

    If name_is_simple is false, we surround <name> with (...) if type is a
    function.
    '''
    if verbose:
        log( '{nest=} {arg_names=} {name=} {type_.spelling=} {type_.get_declaration().get_usr()=}')
        log( '{type_.kind=} {type_.get_array_size()=}')
    def log2( text):
        jlib.log( nest*'    ' + text, 2)

    array_n = type_.get_array_size()
    if verbose:
        log( '{array_n=}')
    if array_n >= 0 or type_.kind == clang.cindex.TypeKind.INCOMPLETEARRAY:
        # Not sure this is correct.
        if verbose: log( '{array_n=}')
        text = declaration_text( type_.get_array_element_type(), name, nest+1, name_is_simple, arg_names, verbose=verbose)
        if array_n < 0:
            array_n = ''
        text += f'[{array_n}]'
        return text

    pointee = type_.get_pointee()
    if pointee and pointee.spelling:
        if verbose: log( '{pointee.spelling=}')
        return declaration_text( pointee, f'*{name}', nest+1, name_is_simple=False, arg_names=arg_names, verbose=verbose)

    if type_.get_typedef_name():
        if verbose: log( '{type_.get_typedef_name()=}')
        const = 'const ' if type_.is_const_qualified() else ''
        return f'{const}{type_.get_typedef_name()} {name}'

    if type_.get_result().spelling:
        log1( 'function: {type_.spelling=} {type_.kind=} {type_.get_result().spelling=} {type_.get_declaration().spelling=}')
        # <type> is a function. We call ourselves with type=type_.get_result()
        # and name=<name>(<args>).
        #
        if 0 and verbose:
            nc = 0
            for nci in type_.get_declaration().get_arguments():
                nc += 1
            nt = 0
            for nti in type_.argument_types():
                nt += 1
            if nt == nc:
                log( '*** {nt=} == {nc=}')
            if nt != nc:
                log( '*** {nt=} != {nc=}')

        ret = ''
        i = 0

        #for arg_cursor in type_.get_declaration().get_arguments():
        #    arg = arg_cursor
        try:
            args = type_.argument_types()
        except Exception as e:
            if 'libclang-6' in g_clang_info.libclang_so:
                raise Clang6FnArgsBug( f'type_.spelling is {type_.spelling}: {e!r}')

        for arg in args:
            if i:
                ret += ', '
            #name2 = arg.spelling if arg_names else ''
            #if verbose:
            #    log( '{name2=}')
            #ret += declaration_text( arg, name2, nest+1, arg_names=arg_names)
            ret += declaration_text( arg, '', nest+1, arg_names=arg_names)
            i += 1
        if verbose: log( '{ret!r=}')
        if not name_is_simple:
            # If name isn't a simple identifier, put it inside braces, e.g.
            # this crudely allows function pointers to work.
            name = f'({name})'
        ret = f'{name}({ret})'
        if verbose: log( '{type_.get_result()=}')
        ret = declaration_text( type_.get_result(), ret, nest+1, name_is_simple=False, arg_names=arg_names, verbose=verbose)
        if verbose:
            log( 'returning {ret=}')
        return ret

    ret = f'{type_.spelling} {name}'
    if verbose: log( 'returning {ret=}')
    return ret


def dump_ast( cursor, depth=0):
    indent = depth*4*' '
    for cursor2 in cursor.get_children():
        jlib.log( indent * ' ' + '{cursor2.kind=} {cursor2.mangled_name=} {cursor2.displayname=} {cursor2.spelling=}')
        dump_ast( cursor2, depth+1)


def show_ast( filename):
    index = clang.cindex.Index.create()
    tu = index.parse( filename,
            args=( '-I', g_clang_info.include_path),
            )
    dump_ast( tu.cursor)


get_args_cache = dict()

def get_args( tu, cursor, include_fz_context=False, verbose=False):
    '''
    Yields information about each arg of the function at <cursor>.

    Args:
        tu:
            A clang.cindex.TranslationUnit instance.
        cursor:
            Clang cursor for the function.
        include_fz_context:
            If false, we skip args that are 'struct fz_context*'
        verbose:
            .

    Yields (arg, name, separator, alt, double_ptr) for each argument of
    function at <cursor>:

        arg:
            Cursor for the argument.
        name:
            Arg name, or an invented name if none was present.
        separator:
            '' for first returned argument, ', ' for the rest.
        alt:
            Cursor for underlying fz_ struct type if <arg> is a pointer to or
            ref/value of a fz_ struct type that we wrap. Else None.
        out_param:
            True if this looks like an out-parameter, e.g. alt is set and
            double pointer, or arg is pointer other than to char.
    '''
    # We are called a few times for each function, and the calculations we do
    # are slow, so we cache the returned items. E.g. this reduces total time of
    # --build 0 from 3.5s to 2.1s.
    #
    key = tu, cursor.location.file, cursor.location.line, include_fz_context
    ret = get_args_cache.get( key)

    if ret is None:
        ret = []
        i = 0
        separator = ''
        for arg in cursor.get_arguments():
            assert arg.kind == clang.cindex.CursorKind.PARM_DECL
            if not include_fz_context and is_pointer_to( arg.type, 'fz_context'):
                # Omit this arg because our generated mupdf_*() wrapping functions
                # use internalContextGet() to get a context.
                continue
            name = arg.mangled_name or f'arg_{i}'
            alt = None
            out_param = False
            # Set <alt> to wrapping class if possible.
            base_type = get_base_type( arg.type)
            base_type_cursor = base_type.get_declaration()
            base_typename = get_base_typename( base_type)
            extras = classextras.get( base_typename)
            if verbose:
                log( '{extras=}')
            if extras:
                if verbose:
                    log( '{extras.opaque=} {base_type_cursor.kind=} {base_type_cursor.is_definition()=}')
                if extras.opaque:
                    # E.g. we don't have access to defintion of fz_separation,
                    # but it is marked in classextras with opaque=true, so
                    # there will be a wrapper class.
                    alt = base_type_cursor
                elif (1
                        and base_type_cursor.kind == clang.cindex.CursorKind.STRUCT_DECL
                        #and base_type_cursor.is_definition()
                        ):
                    alt = base_type_cursor
            if verbose:
                log( '{arg.type.spelling=} {base_type.spelling=}')
            if alt:
                if is_double_pointer( arg.type):
                    out_param = True
            elif get_base_typename( arg.type) in ('char', 'unsigned char', 'signed char', 'void'):
                if is_double_pointer( arg.type):
                    #log( 'setting outparam: {cursor.spelling=} {arg.type=}')
                    if cursor.spelling == 'pdf_clean_file':
                        # Don't mark char** argv as out-param, which will also
                        # allow us to tell swig to convert python lists into
                        # (argc,char**) pair.
                        pass
                    else:
                        out_param = True
            elif base_typename.startswith( ('fz_', 'pdf_')):
                # Pointer to fz_ struct is not usually an out-param.
                if verbose: log( 'not out-param because arg is: {arg.displayname=} {base_type.spelling=} {extras}')
                pass
            elif arg.type.kind == clang.cindex.TypeKind.POINTER:
                if arg.type.get_pointee().get_canonical().kind == clang.cindex.TypeKind.FUNCTIONPROTO:
                    # Don't mark function-pointer args as out-params.
                    pass
                else:
                    out_param = True
            if verbose:
                log( 'returning {(arg.displayname, name, separator, alt, out_param)}')

            #yield arg, name, separator, alt, out_param
            ret.append( (arg, name, separator, alt, out_param))
            i += 1
            separator = ', '

        get_args_cache[ key] = ret

    for i in ret:
        yield i


def fn_has_struct_args( tu, cursor):
    '''
    Returns true if fn at <cursor> takes any fz_* struct args.
    '''
    for arg, name, separator, alt, out_param in get_args( tu, cursor):
        if alt:
            if alt.spelling in omit_class_names0:
                pass
                #log( '*** omitting {alt.spelling=}')
            else:
                return True

def get_first_arg( tu, cursor):
    '''
    Returns (arginfo, n), where <arginfo> is tuple from get_args() for
    first argument (or None if no arguments), and <n> is number of arguments.
    '''
    n = 0
    ret = None
    for arginfo in get_args( tu, cursor):
        if n == 0:
            ret = arginfo
        n += 1
    return ret, n


is_pointer_to_cache = dict()

def is_pointer_to( type_, destination, verbose=False):
    '''
    Returns true if <type> is a pointer to <destination>.

    We do this using text for <destination>, rather than a clang.cindex.Type
    or clang.cindex.Cursor, so that we can represent base types such as int or
    char without having clang parse system headers. This involves stripping any
    initial 'struct ' text.

    Also, clang's representation of mupdf's varying use of typedef, struct and
    forward-declarations is rather difficult to work with directly.

    type_:
        A clang.cindex.Type.
    destination:
        Text typename.
    '''
    # Use cache - reduces time from 0.6s to 0.2.
    #
    key = type_.spelling, destination
    ret = is_pointer_to_cache.get( key)
    if ret is None:
        assert isinstance( type_, clang.cindex.Type)
        ret = None
        destination = clip( destination, 'struct ')
        if type_.kind == clang.cindex.TypeKind.POINTER:
            pointee = type_.get_pointee().get_canonical()

            if verbose:
                jlib.log( "{declaration_text( pointee, '')!r=} {destination + ' '!r=}")
            d = declaration_text( pointee, '')
            d = clip( d, 'const ')
            d = clip( d, 'struct ')
            ret = d == f'{destination} ' or d == f'const {destination} '
        is_pointer_to_cache[ key] = ret

    return ret


def make_fncall( tu, cursor, return_type, fncall, out):
    '''
    Writes a function call to <out>, using fz_context_s from
    internal_context_get() and with fz_try...fz_catch that converts to C++
    exceptions by calling throw_exception().

    return_type:
        Text return type of function, e.g. 'void' or 'double'.
    fncall:
        Text containing function call, e.g. 'function(a, b, 34)'.
    out:
        Stream to which we write generated code.
    '''
    icg = rename.internal( 'context_get')
    te = rename.internal( 'throw_exception')
    out.write(      f'    fz_context* auto_ctx = {icg}();\n')
    out.write(      f'    fz_var(auto_ctx);\n')

    out.write( f'    const char*    trace = getenv("MUPDF_trace");\n')
    out.write( f'    if (trace) {{\n')

    out.write( f'        fprintf(::stderr, "%s:%i:%s(): calling {cursor.mangled_name}():')

    items = []
    for arg, name, separator, alt, out_param in get_args( tu, cursor, include_fz_context=True):
        if is_pointer_to( arg.type, 'fz_context'):
            text = 'auto_ctx=%p'
            value = 'auto_ctx'
        elif is_pointer_to( arg.type, 'char'):
            text = f'{name}=%s'
            value = f'{name}'
        elif arg.type.kind == clang.cindex.TypeKind.POINTER:
            text = f'{name}=%p'
            value = f'{name}'
        elif alt:
            text = f'&{name}=%p'
            value = f'&{name}'
        else:
            text = f'(long){name}=%li'
            value = f'(long){name}'
        items.append( (text, value))

    for text, value in items:
        out.write( f' {text}')

    out.write( '\\n", __FILE__, __LINE__, __FUNCTION__')

    for text, value in items:
        out.write( f', {value}')

    out.write( ');\n')
    out.write( '    }\n')

    if return_type != 'void':
        out.write(  f'    {return_type} ret;\n')
        out.write(  f'    fz_var(ret);\n')
    out.write(      f'    fz_try(auto_ctx) {{\n')
    if return_type == 'void':
        out.write(  f'        {fncall};\n')
    else:
        out.write(  f'        ret = {fncall};\n')
    out.write(      f'    }}\n')
    out.write(      f'    fz_catch(auto_ctx) {{\n')
    out.write(      f'        {te}(auto_ctx);\n')
    out.write(      f'    }}\n')
    if return_type != 'void':
        out.write(  f'    return ret;\n')



def make_function_wrapper( tu, cursor, fnname, out_h, out_cpp):
    '''
    Writes simple C++ wrapper fn, converting any fz_try..fz_catch exception
    into a C++ exception and getting internal context.

    cursor:
        Clang cursor for function to wrap.
    fnname:
        Name of function to create.
    out_h:
        Stream to which we write header output.
    out_cpp:
        Stream to which we write cpp output.

    Example generated function:

        fz_band_writer * mupdf_new_band_writer_of_size(fz_context *ctx, size_t size, fz_output *out)
        {
            fz_band_writer * ret;
            fz_var(ret);
            fz_try(ctx) {
                ret = fz_new_band_writer_of_size(ctx, size, out);
            }
            fz_catch(ctx) {
                mupdf_throw_exception(ctx);
            }
            return ret;
        }
    '''
    assert cursor.kind == clang.cindex.CursorKind.FUNCTION_DECL

    verbose = fnname == 'pdf_add_annot_ink_list'

    # Write first line: <result_type> <fnname> (<args>...)
    #
    for out in out_h, out_cpp:
        out.write( f'/* Wrapper for {cursor.mangled_name}(). */\n')

    # Copy any comment into .h file before declaration.
    if cursor.raw_comment:
        out_h.write( f'{cursor.raw_comment}')
        if not cursor.raw_comment.endswith( '\n'):
            out_h.write( '\n')

    name_args_h = f'{fnname}('
    name_args_cpp = f'{fnname}('
    comma = ''
    for arg, name, separator, alt, out_param in get_args( tu, cursor, include_fz_context=True):
        if verbose:
            log( '{arg=} {name=} {separator=} {alt=} {out_param=}')
        if is_pointer_to(arg.type, 'fz_context'):
            continue
        name2 = name
        if out_param:
            name2 = f'mupdf_OUTPARAM({name})'
        decl = declaration_text( arg.type, name2, verbose=verbose)
        if verbose:
            log( '{decl=}')
        name_args_h += f'{comma}{decl}'
        decl = declaration_text( arg.type, name)
        name_args_cpp += f'{comma}{decl}'
        comma = ', '
    name_args_h += ')'
    name_args_cpp += ')'
    declaration_h = declaration_text( cursor.result_type, name_args_h)
    declaration_cpp = declaration_text( cursor.result_type, name_args_cpp)
    out_h.write( declaration_h)
    out_h.write( ';\n')
    out_h.write( '\n')

    # Write function definition.
    #
    out_cpp.write( declaration_cpp)
    out_cpp.write( '\n')
    out_cpp.write( '{\n')

    return_type = cursor.result_type.spelling
    fncall = ''
    fncall += f'{rename.function_raw(cursor.mangled_name)}('
    for arg, name, separator, alt, out_param in get_args( tu, cursor, include_fz_context=True):
        if is_pointer_to(arg.type, 'fz_context'):
            fncall += f'{separator}auto_ctx'
        else:
            fncall += f'{separator}{name}'
    fncall += ')'
    make_fncall( tu, cursor, return_type, fncall, out_cpp)
    out_cpp.write( '}\n')
    out_cpp.write( '\n')


def make_namespace_open( namespace, out):
    if namespace:
        out.write( f'namespace {namespace}\n')
        out.write( '{\n')
        out.write( '\n')


def make_namespace_close( namespace, out):
    if namespace:
        out.write( f'}} /* End of namespace {namespace}. */\n')
        out.write( '\n')



def make_internal_functions( namespace, out_h, out_cpp):
    '''
    Writes internal support functions.

    out_h:
        Stream to which we write header output.
    out_cpp:
        Stream to which we write cpp output.
    '''
    out_h.write(
            textwrap.dedent(
            f'''
            /* Internal use only. Returns fz_context* for use by current thread. */
            fz_context* {rename.internal('context_get')}();


            '''
            ))

    out_cpp.write(
            textwrap.dedent(
            '''
            #include "mupdf/exceptions.h"
            #include "mupdf/internal.h"

            #include <thread>
            #include <mutex>


            '''))

    make_namespace_open( namespace, out_cpp)

    state_t = rename.internal( 'state')
    thread_state_t = rename.internal( 'thread_state')

    cpp = textwrap.dedent(
            '''
            struct state_t
            {
                state_t()
                {
                    m_locks.user = this;
                    m_locks.lock = lock;
                    m_locks.unlock = unlock;
                    m_ctx = fz_new_context(NULL /*alloc*/, &m_locks, FZ_STORE_DEFAULT);
                    fz_register_document_handlers(m_ctx);
                }
                static void lock(void *user, int lock)
                {
                    state_t*    self = (state_t*) user;
                    self->m_mutexes[lock].lock();
                }
                static void unlock(void *user, int lock)
                {
                    state_t*    self = (state_t*) user;
                    self->m_mutexes[lock].unlock();
                }
                ~state_t()
                {
                    fz_drop_context(m_ctx);
                }

                fz_context*         m_ctx;
                std::mutex          m_mutex;    /* Serialise access to m_ctx. */

                /* Provide thread support to mupdf. */
                std::mutex          m_mutexes[FZ_LOCK_MAX];
                fz_locks_context    m_locks;
            };

            static state_t  s_state;

            struct thread_state_t
            {
                thread_state_t()
                : m_ctx(NULL)
                {}
                fz_context* get_context()
                {
                    if (!m_ctx) {
                        /* Make a context for this thread by cloning the global
                        context. */
                        std::lock_guard<std::mutex> lock( s_state.m_mutex);
                        m_ctx = fz_clone_context(s_state.m_ctx);
                    }
                    return m_ctx;
                }
                ~thread_state_t()
                {
                    if (m_ctx) {
                        fz_drop_context( m_ctx);
                    }
                }
                fz_context* m_ctx;
            };

            static thread_local thread_state_t  s_thread_state;

            fz_context* context_get()
            {
                return s_thread_state.get_context();
            }


            ''')
    cpp = cpp.replace( 'thread_state_t', thread_state_t)
    cpp = cpp.replace( 'state_t', state_t)
    cpp = cpp.replace( 'context_get', rename.internal('context_get'))
    out_cpp.write( cpp)

    make_namespace_close( namespace, out_cpp)


# Maps from <tu> to dict of fnname: cursor.
g_functions_cache = dict()

def functions_cache_populate( tu):
    if tu in g_functions_cache:
        return
    fns = dict()

    for cursor in tu.cursor.get_children():
        if (1
                and cursor.kind == clang.cindex.CursorKind.FUNCTION_DECL
                and (
                    cursor.linkage == clang.cindex.LinkageKind.EXTERNAL
                    or
                    cursor.is_definition()  # Picks up static inline functions.
                    )
                ):
            fnname = cursor.mangled_name
            if fnname.startswith( ('fz_', 'pdf_')) and fnname not in omit_fns:
                fns[ fnname] = cursor

    g_functions_cache[ tu] = fns

def find_functions_starting_with( tu, name_prefix, method):
    '''
    Yields (name, cursor) for all functions in <tu> whose names start with
    name_prefix.
    method:
        If true, we omit names that are in omit_methods
    '''
    functions_cache_populate( tu)
    fn_to_cursor = g_functions_cache[ tu]
    for fnname, cursor in fn_to_cursor.items():
        if method and fnname in omit_methods:
            continue
        if not fnname.startswith( name_prefix):
            continue
        yield fnname, cursor


def find_function( tu, fnname, method):
    '''
    Returns cursor for function called <name> in <tu>, or None if not found.
    '''
    assert ' ' not in fnname, f'fnname={fnname}'
    if method and fnname in omit_methods:
        return
    functions_cache_populate( tu)
    return g_functions_cache[ tu].get( fnname)



class MethodExcludeReason_VARIADIC:
    pass
class MethodExcludeReason_OMIT_CLASS:
    pass
class MethodExcludeReason_NO_EXTRAS:
    pass
class MethodExcludeReason_NO_RAW_CONSTRUCTOR:
    pass
class MethodExcludeReason_NOT_COPYABLE:
    pass
class MethodExcludeReason_NO_WRAPPER_CLASS:
    pass
class MethodExcludeReason_ENUM:
    pass
class MethodExcludeReason_FIRST_ARG_NOT_STRUCT:
    pass

# Maps from <structname> to list of functions satisfying conditions specified
# by find_wrappable_function_with_arg0_type() below.
#
find_wrappable_function_with_arg0_type_cache = None

# Maps from fnname to list of strings, each string being a description of why
# this fn is not suitable for wrapping by class method.
#
find_wrappable_function_with_arg0_type_excluded_cache = None

def find_wrappable_function_with_arg0_type_cache_populate( tu):

    global find_wrappable_function_with_arg0_type_cache
    global find_wrappable_function_with_arg0_type_excluded_cache

    if find_wrappable_function_with_arg0_type_cache:
        return

    t0 = time.time()

    find_wrappable_function_with_arg0_type_cache = dict()
    find_wrappable_function_with_arg0_type_excluded_cache = dict()

    for fnname, cursor in find_functions_starting_with( tu, ('fz_', 'pdf_'), method=True):

        exclude_reasons = []

        if fnname.startswith( 'fz_drop_') or fnname.startswith( 'fz_keep_'):
            continue
        if fnname.startswith( 'pdf_drop_') or fnname.startswith( 'pdf_keep_'):
            continue

        if cursor.type.is_function_variadic():
            exclude_reasons.append(
                    (
                    MethodExcludeReason_VARIADIC,
                    'function is variadic',
                    ))

        # Look at resulttype.
        #
        result_type = cursor.type.get_result().get_canonical()
        if result_type.kind == clang.cindex.TypeKind.POINTER:
            result_type = result_type.get_pointee().get_canonical()
        result_type = clip( result_type.spelling, 'struct ')
        if result_type in omit_class_names0:
            exclude_reasons.append(
                    (
                    MethodExcludeReason_OMIT_CLASS,
                    f'result_type={result_type} is in omit_class_names0',
                    ))
        if result_type.startswith( ('fz_', 'pdf_')):
            result_type_extras = get_fz_extras( result_type)
            if not result_type_extras:
                exclude_reasons.append(
                        (
                        MethodExcludeReason_NO_EXTRAS,
                        f'no extras defined for result_type={result_type}',
                        ))
            else:
                if not result_type_extras.constructor_raw:
                    exclude_reasons.append(
                            (
                            MethodExcludeReason_NO_RAW_CONSTRUCTOR,
                            f'wrapper for result_type={result_type} does not have raw constructor.',
                            ))
                if not result_type_extras.copyable:
                    exclude_reasons.append(
                            (
                            MethodExcludeReason_NOT_COPYABLE,
                                f'wrapper for result_type={result_type} is not copyable.',
                            ))

        # Look at args
        #
        i = 0
        arg0_cursor = None
        for arg, name, separator, alt, out_param in get_args( tu, cursor):

            base_typename = get_base_typename( arg.type)
            if not alt and base_typename.startswith( ('fz_', 'pdf_')):
                if arg.type.get_canonical().kind==clang.cindex.TypeKind.ENUM:
                    # We don't (yet) wrap fz_* enums, but for now at least we
                    # still wrap functions that take fz_* enum parameters -
                    # callers will have to use the fz_* type.
                    #
                    # For example this is required by mutool_draw.py because
                    # mudraw.c calls fz_set_separation_behavior().
                    #
                    log( 'not excluding {fnname=} with enum fz_ param : {arg.spelling=} {arg.type.kind} {arg.type.get_canonical().kind=}')
                    if 0:
                        exclude_reasons.append(
                                (
                                MethodExcludeReason_ENUM,
                                f'arg i={i} is enum: {arg.type.get_canonical().spelling}',
                                ))
                else:
                    exclude_reasons.append(
                            (
                            MethodExcludeReason_NO_WRAPPER_CLASS,
                            f'no wrapper class for arg i={i}: {arg.type.get_canonical().spelling} {arg.type.get_canonical().kind}',
                            ))
            if i == 0:
                if alt:
                    arg0_cursor = alt
                else:
                    exclude_reasons.append(
                            (
                            MethodExcludeReason_FIRST_ARG_NOT_STRUCT,
                            'first arg is not fz_* struct',
                            ))
            i += 1

        if exclude_reasons:
            find_wrappable_function_with_arg0_type_excluded_cache[ fnname] = exclude_reasons
            if 0:
                log( 'excluding {fnname=} because:')
                for i in exclude_reasons:
                    log( '    {i}')
        else:
            if i > 0:
                # <fnname> is ok to wrap.
                arg0 = arg0_cursor.type.get_canonical().spelling
                arg0 = clip( arg0, 'struct ')

                items = find_wrappable_function_with_arg0_type_cache.setdefault( arg0, [])
                items.append( fnname)

    if 0:
        log( f'populating find_wrappable_function_with_arg0_type_cache took {time.time()-t0}s')


def find_wrappable_function_with_arg0_type( tu, structname):
    '''
    Return list of fz_*() function names which could be wrapped as a method of
    our wrapper class for <structname>.

    The functions whose names we return, satisfy all of the following:

        First non-context param is <structname> (by reference, pointer or value).

        No arg type (by reference, pointer or value) is in omit_class_names0.

        Return type (by reference, pointer or value) is not in omit_class_names0.

        If return type is a fz_* struc (by reference, pointer or value), the
        corresponding wrapper class has a raw constructor.
    '''
    find_wrappable_function_with_arg0_type_cache_populate( tu)

    ret = find_wrappable_function_with_arg0_type_cache.get( structname, [])
    return ret



def make_function_wrappers(
        tu,
        namespace,
        out_exceptions_h,
        out_exceptions_cpp,
        out_functions_h,
        out_functions_cpp,
        out_internal_h,
        out_internal_cpp,
        ):
    '''
    Generates C++ source code containing wrappers for all fz_*() functions.

    We also create a function throw_exception(fz_context* ctx) that throws a
    C++ exception appropriate for the error in ctx.

    Wrappers for functions that have first arg fz_context*, convert
    fz_try..fz_catch exceptions into C++ exceptions by calling
    throw_exception(). Others are just plain wrappers.

    We remove any fz_context* argument and make implementation call
    internal_get_context() to get the fz_context* to use.

    We generate a class for each exception type.

    Returned source is just the raw fucntions, e.g. it does not contain
    required #include's.

    Args:
        tu:
            Clang translation unit.
        out_exceptions_h:
            Stream to which we write exception class definitions.
        out_exceptions_cpp:
            Stream to which we write exception class implementation.
        out_functions_h:
            Stream to which we write function declarations.
        out_functions_cpp:
            Stream to which we write function definitions.
    '''

    # Look for FZ_ERROR_* enums. We generate an exception class for each of
    # these.
    #
    error_name_prefix = 'FZ_ERROR_'
    fz_error_names = []
    fz_error_names_maxlen = 0   # Used for padding so generated code aligns.
    for cursor in tu.cursor.get_children():
        if cursor.kind == clang.cindex.CursorKind.ENUM_DECL:
            #log( 'enum: {cursor.spelling=})
            for child in cursor.get_children():
                #log( 'child:{ child.spelling=})
                if child.spelling.startswith( error_name_prefix):
                    name = child.spelling[ len(error_name_prefix):]
                    fz_error_names.append( name)
                    if len( name) > fz_error_names_maxlen:
                        fz_error_names_maxlen = len( name)

    def errors():
        '''
        Yields (enum, typename, padding) for each error.
        E.g.:
            enum=FZ_ERROR_MEMORY
            typename=mupdf_error_memory
            padding='  '
        '''
        for name in fz_error_names:
            enum = f'{error_name_prefix}{name}'
            typename = rename.class_( f'fz_error_{name.lower()}')
            padding = (fz_error_names_maxlen - len(name)) * ' '
            yield enum, typename, padding

    # Declare base exception class and define its methods.
    #
    base_name = rename.class_('fz_error_base')

    out_exceptions_h.write( textwrap.dedent(
            f'''
            /* Base class for {rename.class_( '')} exceptions */
            struct {base_name} : std::exception
            {{
                int         m_code;
                std::string m_text;
                const char* what() const throw();
                {base_name}(int code, const char* text);
            }};
            '''))

    out_exceptions_cpp.write( textwrap.dedent(
            f'''
            {base_name}::{base_name}(int code, const char* text)
            : m_code(code)
            {{
                char    code_text[32];
                snprintf(code_text, sizeof(code_text), "%i", code);
                m_text = std::string("code=") + code_text + ": " + text;
            }};

            const char* {base_name}::what() const throw()
            {{
                return m_text.c_str();
            }};

            '''))

    # Declare exception class for each FZ_ERROR_*.
    #
    for enum, typename, padding in errors():
        out_exceptions_h.write( textwrap.dedent(
                f'''
                /* For {enum}. */
                struct {typename} : {base_name}
                {{
                    {typename}(const char* message);
                }};

                '''))

    # Define constructor for each exception class.
    #
    for enum, typename, padding in errors():
        out_exceptions_cpp.write( textwrap.dedent(
                f'''
                {typename}::{typename}(const char* text)
                : {base_name}({enum}, text)
                {{
                }}

                '''))

    # Generate function that throws an appropriate exception from a fz_context.
    #
    te = rename.internal( 'throw_exception')
    out_exceptions_h.write( textwrap.dedent(
            f'''
            /* Throw exception appropriate for error in <ctx>. */
            void {te}(fz_context* ctx);

            '''))
    out_exceptions_cpp.write( textwrap.dedent(
            f'''
            void {te}(fz_context* ctx)
            {{
                int code = fz_caught(ctx);
                const char* text = fz_caught_message(ctx);
            '''))
    for enum, typename, padding in errors():
        out_exceptions_cpp.write( f'    if (code == {enum}) {padding}throw {typename}{padding}(text);\n')
    out_exceptions_cpp.write( f'    throw {base_name}(code, fz_caught_message(ctx));\n')
    out_exceptions_cpp.write( f'}}\n')
    out_exceptions_cpp.write( '\n')

    make_internal_functions( namespace, out_internal_h, out_internal_cpp)

    # Generate wrappers for each function that we find.
    #
    functions = []
    for fnname, cursor in find_functions_starting_with( tu, ('fz_', 'pdf_'), method=False):
        assert fnname not in omit_fns
        if cursor.type.is_function_variadic():
            # We don't attempt to wrap variadic functions - would need to find
            # the equivalent function that takes a va_list.
            continue

        functions.append( (fnname, cursor))

    log( '{len(functions)=}')

    # Sort by function-name to make output easier to read.
    functions.sort()
    for fnname, cursor in functions:
        fnname_wrapper = rename.function( fnname)
        # clang-6 appears not to be able to handle fn args that are themselves
        # function pointers, so for now we allow make_function_wrapper() to
        # fail, so we need to use temporary buffers, otherwise out_functions_h
        # and out_functions_cpp can get partial text written.
        #
        temp_out_h = io.StringIO()
        temp_out_cpp = io.StringIO()
        try:
            make_function_wrapper(
                    tu,
                    cursor,
                    fnname_wrapper,
                    temp_out_h,
                    temp_out_cpp,
                    )
        except Clang6FnArgsBug as e:
            #log( jlib.exception_info())
            log( 'Unable to wrap function {cursor.spelling} becase: {e}')
        else:
            out_functions_h.write( temp_out_h.getvalue())
            out_functions_cpp.write( temp_out_cpp.getvalue())


find_struct_cache = None
def find_struct( tu, structname, require_definition=True):
    '''
    Finds definition of structure.
    Args:
        tu:
            Translation unit.
        structname:
            Name of structure to find.
        require_definition:
            Only return cursor if it is for definition of structure.

    Returns cursor for definition or None.
    '''
    structname = clip( structname, 'struct ')   # Remove any 'struct ' prefix.
    global find_struct_cache
    if find_struct_cache is None:
        find_struct_cache = dict()
        for cursor in tu.cursor.get_children():
            already = find_struct_cache.get( cursor.spelling)
            if already is None:
                find_struct_cache[ cursor.spelling] = cursor
            elif cursor.is_definition() and not already.is_definition():
                find_struct_cache[ cursor.spelling] = cursor
    ret = find_struct_cache.get( structname)
    if not ret:
        return
    if require_definition and not ret.is_definition():
        return
    return ret


def find_name( cursor, name, nest=0):
    '''
    Returns cursor for specified name within <cursor>, or None if not found.

    name:
        Name to search for. Can contain '.' characters; we look for each
        element in turn, calling ourselves recursively.

    cursor:
        Item to search.
    '''
    if cursor.spelling == '':
        # Anonymous item; this seems to occur for (non-anonymous) unions.
        #
        # We recurse into children directly.
        #
        for c in cursor.get_children():
            ret = find_name_internal( c, name, nest+1)
            if ret:
                return ret

    d = name.find( '.')
    if d >= 0:
        head, tail = name[:d], name[d+1:]
        # Look for first element then for remaining.
        c = find_name( cursor, head, nest+1)
        if not c:
            return
        ret = find_name( c, tail, nest+2)
        return ret

    for c in cursor.type.get_canonical().get_fields():
        if c.spelling == '':
            ret = find_name( c, name, nest+1)
            if ret:
                return ret
        if c.spelling == name:
            return c



def class_add_iterator( struct, structname, classname, extras):
    '''
    Add begin() and end() methods so that this generated class is iterable
    from C++ with:

        for (auto i: foo) {...}

    We modify <extras> to create an iterator class and add begin() and end()
    methods that each return an instance of the iterator class.
    '''
    it_begin, it_end = extras.iterator_next

    # Figure out type of what the iterator returns by looking at type of
    # <it_begin>.
    if it_begin:
        c = find_name( struct, it_begin)
        assert c.type.kind == clang.cindex.TypeKind.POINTER
        it_internal_type = c.type.get_pointee().get_canonical().spelling
        it_internal_type = clip( it_internal_type, 'struct ')
        it_type = rename.class_( it_internal_type)
    else:
        # The container is also the first item in the linked list.
        it_internal_type = structname
        it_type = classname

    # Iterator needs raw constructor so it can set its internal m_item as
    # it iterates.
    #
    extras.constructor_raw = True

    # We add to extras.methods_extra().
    #
    extras.methods_extra.append(
            ExtraMethod( f'{classname}Iterator', 'begin()',
                    f'''
                    {{
                        return {classname}Iterator(m_internal{'->'+it_begin if it_begin else ''});
                    }}
                    ''',
                    f'/* Used for iteration over linked list of {it_type} items starting at {it_internal_type}::{it_begin}. */',
                    ),
            )
    extras.methods_extra.append(
            ExtraMethod( f'{classname}Iterator', 'end()',
                    f'''
                    {{
                        return {classname}Iterator(NULL);
                    }}
                    ''',
                    f'/* Used for iteration over linked list of {it_type} items starting at {it_internal_type}::{it_begin}. */',
                    ),
            )

    extras.class_bottom += f'\n    typedef {classname}Iterator iterator;\n'

    extras.class_pre += f'struct {classname}Iterator;\n'

    extras.class_post += f'''
            struct {classname}Iterator
            {{
                {classname}Iterator({it_internal_type}* item);
                {classname}Iterator& operator++();
                bool operator==( const {classname}Iterator& rhs);
                bool operator!=( const {classname}Iterator& rhs);
                {it_type} operator*();
                {it_type}* operator->();
                private:
                {it_type} m_item;
            }};
            '''

    extras.extra_cpp += f'''
            {classname}Iterator::{classname}Iterator({it_internal_type}* item)
            : m_item( item)
            {{
            }}
            {classname}Iterator& {classname}Iterator::operator++()
            {{
                m_item = {it_type}(m_item.m_internal->next);
                return *this;
            }}
            bool {classname}Iterator::operator==( const {classname}Iterator& rhs)
            {{
                return m_item.m_internal == rhs.m_item.m_internal;
            }}
            bool {classname}Iterator::operator!=( const {classname}Iterator& rhs)
            {{
                return m_item.m_internal != rhs.m_item.m_internal;
            }}
            {it_type} {classname}Iterator::operator*()
            {{
                return m_item;
            }}
            {it_type}* {classname}Iterator::operator->()
            {{
                return &m_item;
            }}

            void test({classname}& item)
            {{
                for( {classname}Iterator it = item.begin(); it != item.end(); ++it) {{
                    (void) *it;
                }}
                for ( auto i: item) {{
                    (void) i;
                }}
            }}

            '''

def class_find_constructor_fns( tu, classname, structname, base_name, extras):
    '''
    Returns list of functions that could be used by constructor.

    tu:
        .
    classname:
        Name of our wrapper class.
    structname:
        Name of underlying mupdf struct.
    base_name:
        Name of struct without 'fz_' prefix.
    extras:
        .
    '''
    assert structname == f'fz_{base_name}' or structname == f'pdf_{base_name}'
    constructor_fns = []
    if '-' not in extras.constructor_prefixes:
        # Add default constructor fn prefix.
        if structname.startswith( 'fz_'):
            extras.constructor_prefixes.insert( 0, f'fz_new_')
        elif structname.startswith( 'pdf_'):
            extras.constructor_prefixes.insert( 0, f'pdf_new_')
    for fnprefix in extras.constructor_prefixes:
        for fnname, cursor in find_functions_starting_with( tu, fnprefix, method=True):
            # Check whether this has identical signature to any fn we've
            # already found.
            duplicate_type = None
            duplicate_name = False
            for f, c, is_duplicate in constructor_fns:
                #jlib.log( '{cursor.type=} {c.type=}')
                if f == fnname:
                    duplicate_name = True
                    break
                if c.type == cursor.type:
                    #jlib.log( '{structname} wrapper: ignoring candidate constructor {fnname}() because prototype is indistinguishable from {f=}()')
                    duplicate_type = f
                    break
            if duplicate_name:
                continue
            ok = False
            if fnname in extras.constructor_excludes:
                pass
            elif extras.pod and cursor.result_type.get_canonical().spelling == f'{structname}':
                ok = True
            elif not extras.pod and is_pointer_to( cursor.result_type, f'{structname}'):
                ok = True

            if ok:
                if duplicate_type and extras.copyable:
                    log1( 'adding static method wrapper for {fnname}')
                    extras.method_wrappers_static.append( fnname)
                else:
                    if duplicate_type:
                        log( 'not able to provide static factory fn {structname}::{fnname} because wrapper class is not copyable.')
                        pass
                    log1( 'adding constructor wrapper for {fnname}')
                    constructor_fns.append( (fnname, cursor, duplicate_type))
            else:
                log3( 'ignoring possible constructor for {classname=} because does not return required type: {fnname=} -> {cursor.result_type.spelling=}')
                pass

    constructor_fns.sort()
    return constructor_fns


def class_find_destructor_fns( tu, structname, base_name):
    '''
    Returns list of functions that could be used by destructor - must be called
    'fz_drop_<typename>', must take a <struct>* arg, may take a fz_context*
    arg.
    '''
    if structname.startswith( 'fz_'):
        destructor_prefix = f'fz_drop_{base_name}'
    elif structname.startswith( 'pdf_'):
        destructor_prefix = f'pdf_drop_{base_name}'
    destructor_fns = []
    for fnname, cursor in find_functions_starting_with( tu, destructor_prefix, method=True):
        arg_struct = False
        arg_context = False
        args_num = 0
        for arg, name, separator, alt, out_param in get_args( tu, cursor):
            if not arg_struct and is_pointer_to( arg.type, structname):
                arg_struct = True
            elif not arg_context and is_pointer_to( arg.type, 'fz_context'):
                arg_context = True
            args_num += 1
        if arg_struct:
            if args_num == 1 or (args_num == 2 and arg_context):
                # No params other than <struct>* and fz_context* so this is
                # candidate destructor.
                #log( 'adding candidate destructor: {fnname}')
                fnname = rename.function( fnname)
                destructor_fns.append( (fnname, cursor))

    destructor_fns.sort()
    return destructor_fns


def class_copy_constructor(
        tu,
        functions,
        structname,
        base_name,
        classname,
        constructor_fns,
        out_h,
        out_cpp,
        ):
    '''
    Generate a copy constructor and operator= by finding a suitable fz_keep_*()
    function.

    We raise an exception if we can't find one.
    '''
    if structname.startswith( 'fz_'):
        keep_name = f'fz_keep_{base_name}'
        drop_name = f'fz_drop_{base_name}'
    elif structname.startswith( 'pdf_'):
        keep_name = f'pdf_keep_{base_name}'
        drop_name = f'pdf_drop_{base_name}'
    for name in keep_name, drop_name:
        cursor = find_function( tu, name, method=True)
        if not cursor:
            #log( 'marking non-copyable: {structname}, because no function {name}().')
            classextras.get( structname).copyable = False
            return
        if name == keep_name:
            pvoid = is_pointer_to( cursor.result_type, 'void')
            assert ( pvoid
                    or is_pointer_to( cursor.result_type, structname)
                    ), (
                    f'result_type not void* or pointer to {name}: {cursor.result_type.spelling}'
                    )
        (arg, name, separator, alt, out_param), n = get_first_arg( tu, cursor)
        assert n == 1, f'should take exactly one arg: {cursor.spelling}()'
        assert is_pointer_to( arg.type, structname), (
                f'arg0 is not pointer to {structname}: {cursor.spelling}(): {arg.spelling} {name}')

    for fnname, cursor, duplicate_type in constructor_fns:
        fnname2 = rename.function_call(fnname)
        if fnname2 == keep_name:
            log( 'not generating copy constructor with {keep_name=} because already used by a constructor.')
            break
    else:
        functions( keep_name)
        comment = f'Copy constructor using {keep_name}().'
        out_h.write( '\n')
        out_h.write( f'    /* {comment} */\n')
        out_h.write( f'    {classname}(const {classname}& rhs);\n')
        out_h.write( '\n')

        cast = ''
        if pvoid:
            # Need to cast the void* to the correct type.
            cast = f'({structname}*) '

        out_cpp.write( f'/* {comment} */\n')
        out_cpp.write( f'{classname}::{classname}(const {classname}& rhs)\n')
        out_cpp.write( f': m_internal({cast}{rename.function_call(keep_name)}(rhs.m_internal))\n')
        out_cpp.write( '{\n')
        out_cpp.write( '}\n')
        out_cpp.write( '\n')

    # Make operator=().
    #
    comment = f'operator= using {keep_name}() and {drop_name}().'
    out_h.write( f'    /* {comment} */\n')
    out_h.write( f'    {classname}& operator=(const {classname}& rhs);\n')

    out_cpp.write( f'/* {comment} */\n')
    out_cpp.write( f'{classname}& {classname}::operator=(const {classname}& rhs)\n')
    out_cpp.write(  '{\n')
    out_cpp.write( f'    {rename.function_call(drop_name)}(this->m_internal);\n')
    out_cpp.write( f'    {rename.function_call(keep_name)}(rhs.m_internal);\n')
    out_cpp.write( f'    this->m_internal = {cast}rhs.m_internal;\n')
    out_cpp.write( f'    return *this;\n')
    out_cpp.write(  '}\n')
    out_cpp.write(  '\n')


def class_write_method_body(
        tu,
        structname,
        classname,
        fnname,
        out_cpp,
        static,
        constructor,
        extras,
        struct,
        fn_cursor,
        construct_from_temp,
        fnname2,
        return_cursor,
        return_type,
        ):
    out_cpp.write( f'{{\n')

    # Write function call.
    if constructor:
        if extras.pod:
            if extras.pod == 'inline':
                out_cpp.write( f'    *({structname}*) &this->{get_field0(struct.type).spelling} = ')
            else:
                out_cpp.write( f'    this->m_internal = ')
            if fn_cursor.result_type.kind == clang.cindex.TypeKind.POINTER:
                out_cpp.write( f'*')
        else:
            out_cpp.write( f'    this->m_internal = ')
            if fn_cursor.result_type.kind == clang.cindex.TypeKind.POINTER:
                pass
            else:
                assert 0, 'cannot handle underlying fn returning by value when not pod.'
        out_cpp.write( f'{rename.function_call(fnname)}(')

    elif construct_from_temp == 'address_of_value':
        out_cpp.write( f'    {return_cursor.spelling} temp = mupdf::{fnname2}(')
    elif construct_from_temp == 'pointer':
        out_cpp.write( f'    {return_cursor.spelling}* temp = mupdf::{fnname2}(')
    else:
        out_cpp.write( f'    return mupdf::{fnname2}(')

    have_used_this = False
    for cursor_arg, name, separator, alt, out_param in get_args( tu, fn_cursor):
        arg_classname = classname
        if static or constructor:
            arg_classname = None
        have_used_this = write_call_arg(
                cursor_arg,
                name,
                separator,
                alt,
                out_param,
                arg_classname,
                have_used_this,
                out_cpp,
                )
    out_cpp.write( f');\n')

    if fnname in functions_that_return_non_kept:
        return_type_base = clip( return_cursor.spelling, ('fz_', 'pdf_'))
        keep_fn = f'{prefix(structname)}keep_{return_type_base}'
        out_cpp.write( f'    {rename.function_call(keep_fn)}(temp);\n')

    if construct_from_temp == 'address_of_value':
        out_cpp.write( f'    return {return_type}(&temp);\n')
    elif construct_from_temp == 'pointer':
        out_cpp.write( f'    return {return_type}(temp);\n')

    out_cpp.write( f'}}\n')
    out_cpp.write( f'\n')

def class_write_method(
        tu,
        register_fn_use,
        structname,
        classname,
        fnname,
        out_h,
        out_cpp,
        static=False,
        constructor=False,
        extras=None,
        struct=None,
        duplicate_type=None,
        ):
    '''
    Writes a method that calls <fnname>.
        tu
            .
        register_fn_use
            Callback to keep track of what fz_*() fns have been used.
        structname
            E.g. fz_rect.
        classname
            E.g. Rect.
        fnname
            Name of fz_*() fn to wrap, e.g. fz_concat.
        out_h
        out_cpp
            Where to write generated code.
        static
            If true, we generate a static method.

            Otherwise we generate a normal class method, where first arg
            that is type <structname> is omitted from the generated method's
            prototype; in the implementation we use <this>.
        constructor
            If true, we write a constructor.
        extras
            None or ClassExtras instance.
            Only used if <constructor> is true.
        struct=None
            None or cursor for the struct definition.
            Only used if <constructor> is true.
    '''
    assert fnname not in omit_methods
    verbose = False
    logx( '{classname=} {fnname=}')
    assert fnname.startswith( ('fz_', 'pdf_'))
    fn_cursor = find_function( tu, fnname, method=True)
    if not fn_cursor:
        log( '*** ignoring {fnname=}')
        return

    # Construct prototype fnname(args).
    #
    methodname = rename.method( structname, fnname)
    if constructor:
        decl_h = f'{classname}('
        decl_cpp = f'{classname}('
    else:
        decl_h = f'{methodname}('
        decl_cpp = f'{methodname}('
    have_used_this = False
    comma = ''
    for arg, name, separator, alt, out_param in get_args( tu, fn_cursor):
        decl_h += comma
        decl_cpp += comma
        if alt:
            # This parameter is something like 'fz_foo* arg',
            # which we convert to 'mupdf_foo_s& arg' so that the caller can
            # use C++ class mupdf_foo_s.
            #
            if (1
                    and not static
                    and not constructor
                    and rename.class_(clip( alt.type.spelling, 'struct ')) == classname
                    and not have_used_this
                    ):
                assert not out_param
                # Omit this arg from the method's prototype - we'll use <this>
                # when calling the underlying fz_ function.
                have_used_this = True
                continue

            const = ''
            if not out_param:
                extras2 = classextras.get( alt.type.spelling)
                if not extras2:
                    log('cannot find {alt.spelling=} {arg.type.spelling=} {name=}')
            if not out_param and not classextras.get( alt.type.spelling).pod:
                const = 'const '
            decl_h +=   f'{const}{rename.class_(alt.type.spelling)}& '
            if out_param:
                decl_h += f'mupdf_OUTPARAM({name})'
            else:
                decl_h += f'{name}'
            decl_cpp += f'{const}{rename.class_(alt.type.spelling)}& {name}'
        else:
            if out_param:
                decl_h += declaration_text( arg.type, f'mupdf_OUTPARAM({name})')
            else:
                logx( '{arg.spelling=}')
                decl_h += declaration_text( arg.type, name)
            decl_cpp += declaration_text( arg.type, name)
        comma = ', '

    decl_h += ')'
    decl_cpp += ')'

    fnname2 = rename.function( fnname)
    if constructor:
        comment = f'Constructor using {fnname}().'
    else:
        comment = f'Wrapper for {fnname}().'

    if not static and not constructor:
        assert have_used_this, f'error: wrapper for {structname}: {fnname}() is not useful - does not have a {structname} arg.'

    if not duplicate_type:
        register_fn_use( fnname)

    # If this is true, we explicitly construct a temporary from what the
    # wrapped function returns.
    #
    construct_from_temp = None

    warning_not_copyable = False
    warning_no_raw_constructor = False

    return_cursor = None
    return_type = None
    if constructor:
        fn_h = f'{decl_h}'
        fn_cpp = f'{classname}::{decl_cpp}'
    else:
        fn_h = declaration_text( fn_cursor.result_type, decl_h)
        fn_cpp = declaration_text( fn_cursor.result_type, f'{classname}::{decl_cpp}')

        # See whether we can convert return type to an instance of a wrapper
        # class.
        #
        if fn_cursor.result_type.kind == clang.cindex.TypeKind.POINTER:
            t = fn_cursor.result_type.get_pointee().get_canonical()
            return_cursor = find_struct( tu, t.spelling, require_definition=False)
            if return_cursor:
                return_extras = classextras.get( return_cursor.spelling)
                if return_extras:
                    # Change return type to be instance of class wrapper.
                    return_type = rename.class_(return_cursor.spelling)
                    if return_extras.copyable and return_extras.constructor_raw:
                        fn_h = f'{return_type} {decl_h}'
                        fn_cpp = f'{return_type} {classname}::{decl_cpp}'
                        construct_from_temp = 'pointer'
                    else:
                        if not return_extras.copyable:
                            warning_not_copyable = True
                        if not return_extras.constructor_raw:
                            warning_no_raw_constructor = True
        else:
            # The fz_*() function returns by value. See whether we can convert its
            # return type to an instance of a wrapping class.
            #
            # If so, we will use constructor that takes pointer to the fz_
            # struct. C++ doesn't allow us to use address of temporary, so we
            # generate code like this:
            #
            #   fz_quad_s ret = mupdf_snap_selection(...);
            #   return Quad(&ret);
            #
            t = fn_cursor.result_type.get_canonical()
            return_cursor = find_struct( tu, t.spelling)
            if return_cursor:
                tt = return_cursor.type.get_canonical()
                if tt.kind == clang.cindex.TypeKind.ENUM:
                    # For now, we return this type directly with no wrapping.
                    pass
                else:
                    return_extras = classextras.get( return_cursor.type.spelling)
                    if 1 or (return_extras and return_extras.constructor_raw):
                        return_type = rename.class_(return_cursor.type.spelling)
                        fn_h = f'{return_type} {decl_h}'
                        fn_cpp = f'{return_type} {classname}::{decl_cpp}'
                        construct_from_temp = 'address_of_value'
                    else:
                        warning_no_raw_constructor = True

    if warning_not_copyable:
        log( '*** warning: {classname}::{decl_h}: Not able to return wrapping class {return_type} from {return_cursor.spelling} because {return_type} is not copyable.')
    if warning_no_raw_constructor:
        log( '*** warning: {classname}::{decl_h}: Not able to return wrapping class {return_type} from {return_cursor.spelling} because {return_type} has no raw constructor.')

    out_h.write( '\n')
    out_h.write( f'    /* {comment} */\n')

    # Copy any comment (indented) into class definition above method
    # declaration.
    if fn_cursor.raw_comment:
        for line in fn_cursor.raw_comment.split( '\n'):
            out_h.write( f'    {line}\n')

    if duplicate_type:
        out_h.write( f'    /* Disabled because same args as {duplicate_type}.\n')

    out_h.write( f'    {"static " if static else ""}{fn_h};\n')
    if duplicate_type:
        out_h.write( f'    */\n')

    out_cpp.write( f'/* {comment} */\n')
    if duplicate_type:
        out_cpp.write( f'/* Disabled because same args as {duplicate_type}.\n')

    out_cpp.write( f'{fn_cpp}\n')

    class_write_method_body(
            tu,
            structname,
            classname,
            fnname,
            out_cpp,
            static,
            constructor,
            extras,
            struct,
            fn_cursor,
            construct_from_temp,
            fnname2,
            return_cursor,
            return_type,
            )

    if duplicate_type:
        out_cpp.write( f'*/\n')


def class_custom_method( register_fn_use, classname, extramethod, out_h, out_cpp):
    '''
    Writes custom method as specified by <extramethod>.
    '''
    is_constructor = False
    is_destructor = False
    is_begin_end = False

    if extramethod.return_:
        name_args = extramethod.name_args
        return_space = f'{extramethod.return_} '
        comment = 'Custom method.'
        if name_args.startswith( 'begin(') or name_args.startswith( 'end('):
            is_begin_end = True
    elif extramethod.name_args == '~()':
        # Destructor.
        name_args = f'~{classname}{extramethod.name_args[1:]}'
        return_space = ''
        comment = 'Custom destructor.'
        is_destructor = True
    else:
        # Constructor.
        assert extramethod.name_args.startswith( '('), f'bad constructor/destructor in classname={classname}'
        name_args = f'{classname}{extramethod.name_args}'
        return_space = ''
        comment = 'Custom constructor.'
        is_constructor = True

    out_h.write( f'\n')
    if extramethod.comment:
        out_h.write( f'    {extramethod.comment}\n')
    else:
        out_h.write( f'    /* {comment} */\n')
    out_h.write( f'    {return_space}{name_args};\n')

    out_cpp.write( f'/* {comment} */\n')
    out_cpp.write( f'{return_space}{classname}::{name_args}')
    out_cpp.write( textwrap.dedent(extramethod.body))
    out_cpp.write( f'\n')

    if 1:
        # Register calls of all fz_* functions. Not necessarily helpful - we
        # might only be interested in calls of fz_* functions that are directly
        # available to uses of class.
        #
        for fnname in re.findall( '(mupdf::[a-zA-Z0-9_]+) *[(]', extramethod.body):
            fnname = clip( fnname, 'mupdf::')
            if not fnname.startswith( 'pdf_'):
                fnname = 'fz_' + fnname
            #log( 'registering use of {fnname} in extramethod {classname}::{name_args}')
            register_fn_use( fnname)

    return is_constructor, is_destructor, is_begin_end


def class_raw_constructor(
        register_fn_use,
        classname,
        struct,
        structname,
        base_name,
        extras,
        constructor_fns,
        out_h,
        out_cpp,
        ):
    '''
    Create a raw constructor - a constructor taking pointer to underlying
    struct. If constructor_fns includes a fz_keep_*-style fn, we use
    that, otherwise we generate a constructor that simply copies the arg.
    '''
    #jlib.log( 'Creating raw constructor {classname=} {structname=} {extras.pod=} {extras.constructor_raw=} {fnname=}')
    comment = f'/* Constructor using raw copy of pre-existing {structname}. */'
    if extras.pod:
        constructor_decl = f'{classname}(const {structname}* internal)'
    else:
        constructor_decl = f'{classname}({structname}* internal)'
    out_h.write( '\n')
    out_h.write( f'    {comment}\n')
    if extras.constructor_raw == 'default':
        out_h.write( f'    {classname}({structname}* internal=NULL);\n')
    else:
        out_h.write( f'    {constructor_decl};\n')

    out_cpp.write( f'{classname}::{constructor_decl}\n')
    if extras.pod == 'inline':
        pass
    elif extras.pod:
        out_cpp.write( ': m_internal(*internal)\n')
    else:
        out_cpp.write( ': m_internal(internal)\n')
    out_cpp.write( '{\n')
    if extras.pod == 'inline':
        assert struct, f'cannot form raw constructor for inline pod {classname} without cursor for underlying {structname}'
        for c in struct.type.get_canonical().get_fields():
            if c.type.kind == clang.cindex.TypeKind.CONSTANTARRAY:
                out_cpp.write( f'    memcpy(this->{c.spelling}, internal->{c.spelling}, sizeof(this->{c.spelling}));\n')
            else:
                out_cpp.write( f'    this->{c.spelling} = internal->{c.spelling};\n')
    out_cpp.write( '}\n')
    out_cpp.write( '\n')

    if extras.pod == 'inline':
        # Write second constructor that takes underlying struct by value.
        #
        constructor_decl = f'{classname}(const {structname} internal)'
        out_h.write( '\n')
        out_h.write( f'    {comment}\n')
        out_h.write( f'    {constructor_decl};\n')

        out_cpp.write( f'{classname}::{constructor_decl}\n')
        out_cpp.write( '{\n')
        for c in struct.type.get_canonical().get_fields():
            if c.type.kind == clang.cindex.TypeKind.CONSTANTARRAY:
                out_cpp.write( f'    memcpy(this->{c.spelling}, &internal.{c.spelling}, sizeof(this->{c.spelling}));\n')
            else:
                out_cpp.write( f'    this->{c.spelling} = internal.{c.spelling};\n')
        out_cpp.write( '}\n')
        out_cpp.write( '\n')

        # Write accessor for inline state.
        #
        for const in False, True:
            space_const = ' const' if const else ''
            const_space = 'const ' if const else ''
            out_h.write( '\n')
            out_h.write( f'    /* Access as underlying struct. */\n')
            out_h.write( f'    {const_space}{structname}* internal(){space_const};\n')
            out_cpp.write( f'{comment}\n')
            out_cpp.write( f'{const_space}{structname}* {classname}::internal(){space_const}\n')
            out_cpp.write( '{\n')
            field0 = get_field0(struct.type).spelling
            out_cpp.write( f'    return ({const_space}{structname}*) &this->{field0};\n')
            out_cpp.write( '}\n')
            out_cpp.write( '\n')



def class_accessors(
        tu,
        register_fn_use,
        classname,
        struct,
        structname,
        extras,
        out_h,
        out_cpp,
        ):
    '''
    Writes accessor functions for member data.
    '''
    if not extras.pod:
        if 0:
            log( 'creating accessor for non-pod class {classname=} wrapping {structname}')
    for cursor in struct.type.get_canonical().get_fields():
        #jlib.log( 'accessors: {cursor.spelling=} {cursor.type.spelling=}')

        # We set this to fz_keep_<type>() function to call, if we return a
        # wrapper class constructed from raw pointer to underlying fz_* struct.
        keep_function = None

        # Set <decl> to be prototype with %s where the name is, e.g. 'int
        # %s()'; later on we use python's % operator to replace the '%s'
        # with the name we want.
        #
        if cursor.type.kind == clang.cindex.TypeKind.POINTER:
            decl = 'const ' + declaration_text( cursor.type, '%s()')
            pointee_type = cursor.type.get_pointee().get_canonical().spelling
            pointee_type = clip( pointee_type, 'const ')
            pointee_type = clip( pointee_type, 'struct ')
            #if 'fz_' in pointee_type:
            #    log( '{pointee_type=}')
            if pointee_type.startswith( ('fz_', 'pdf_')):
                extras2 = get_fz_extras( pointee_type)
                if extras2:
                    # Make this accessor return an instance of the wrapping
                    # class by value.
                    #
                    classname2 = rename.class_( pointee_type)
                    decl = f'{classname2} %s()'

                    # If there's a fz_keep_() function, we must call it on the
                    # internal data before returning the wrapping class.
                    pointee_type_base = clip( pointee_type, ('fz_', 'pdf_'))
                    keep_function = f'{prefix(pointee_type)}keep_{pointee_type_base}'
                    if find_function( tu, keep_function, method=False):
                        log( 'using {keep_function=}')
                    else:
                        log( 'cannot find {keep_function=}')
                        keep_function = None
        else:
            if 0 and extras.pod:
                # Return reference so caller can modify. Unfortunately SWIG
                # converts non-const references to pointers, so generated
                # python isn't useful.
                fn_args = '& %s()'
            else:
                fn_args = '%s()'
            if cursor.type.get_array_size() >= 0:
                if 0:
                    # Return reference to the array; we need to put fn name
                    # and args inside (...) to allow the declaration syntax
                    # to work - we end up with things like:
                    #
                    #   char (& media_class())[64];
                    #
                    # Unfortunately SWIG doesn't seem to be able to cope
                    # with this.
                    decl = declaration_text( cursor.type, '(%s)' % fn_args)
                else:
                    # Return pointer to the first element of the array, so
                    # that SWIG can cope.
                    fn_args = '* %s()'
                    type_ = cursor.type.get_array_element_type()
                    decl = declaration_text( type_, fn_args)
            else:
                decl = declaration_text( cursor.type, fn_args)

        out_h.write( f'    {decl % cursor.spelling};\n')
        out_cpp.write( '%s\n' % (decl % ( f'{classname}::{cursor.spelling}')))
        out_cpp.write( '{\n')
        if keep_function:
            out_cpp.write( f'    {rename.function_call(keep_function)}(m_internal->{cursor.spelling});\n')
        if extras.pod:
            out_cpp.write( f'    return m_internal.{cursor.spelling};\n')
        else:
            out_cpp.write( f'    return m_internal->{cursor.spelling};\n')
        out_cpp.write( '}\n')
        out_cpp.write( '\n')


def class_destructor(
        register_fn_use,
        classname,
        destructor_fns,
        out_h,
        out_cpp,
        ):
    if len(destructor_fns) > 1:
        # Use function with shortest name.
        if 0:
            jlib.log( 'Multiple possible destructor fns for {classname=}')
            for fnname, cursor in destructor_fns:
                jlib.log( '    {fnname=} {cursor.spelling=}')
        shortest = None
        for i in destructor_fns:
            if shortest is None or len(i[0]) < len(shortest[0]):
                shortest = i
        #jlib.log( 'Using: {shortest[0]=}')
        destructor_fns = [shortest]
    if len(destructor_fns):
        fnname, cursor = destructor_fns[0]
        register_fn_use( cursor.spelling)
        out_h.write( f'    /* Destructor using {cursor.spelling}(). */\n')
        out_h.write( f'    ~{classname}();\n');

        out_cpp.write( f'{classname}::~{classname}()\n')
        out_cpp.write(  '{\n')
        out_cpp.write( f'    {rename.function_call(fnname)}(m_internal);\n')
        out_cpp.write(  '}\n')
        out_cpp.write( '\n')
    else:
        out_h.write( '    /* We use default destructor. */\n')



def class_wrapper( tu, register_fn_use, struct, structname, classname, out_h, out_cpp, out_h_end):
    '''
    Creates source for a class called <classname> that wraps <struct>, with
    methods that call selected fz_*() functions. Writes to out_h and out_cpp.

    Created source is just the per-class code, e.g. it does not contain
    #include's.

    Args:
        tu:
            Clang translation unit.
        struct:
            Cursor for struct to wrap.
        structname:
            Name of struct to wrap.
        classname:
            Name of wrapper class to create.
        out_h:
            Stream to which we write class definition.
        out_cpp:
            Stream to which we write method implementations.
        out_h_end:
            Stream for text that should be put at the end of the generated
            header text.

    Returns <is_container>, true if generated class has custom begin() and
    end() methods.
    '''
    extras = classextras.get( structname)
    assert extras, f'extras is None for {structname}'
    if extras.iterator_next:
        class_add_iterator( struct, structname, classname, extras)

    if extras.class_pre:
        out_h.write( textwrap.dedent( extras.class_pre))

    base_name = clip( structname, ('fz_', 'pdf_'))

    constructor_fns = class_find_constructor_fns( tu, classname, structname, base_name, extras)
    for fnname in extras.constructors_wrappers:
        cursor = find_function( tu, fnname, method=True)
        constructor_fns.append( (fnname, cursor, None))

    destructor_fns = class_find_destructor_fns( tu, structname, base_name)

    # Class definition beginning.
    #
    out_h.write( '\n')
    out_h.write( f'/* Wrapper class for struct {structname}. */\n')
    if struct.raw_comment:
        out_h.write( f'{struct.raw_comment}')
        if not struct.raw_comment.endswith( '\n'):
            out_h.write( '\n')
    out_h.write( f'struct {classname}\n{{')

    out_cpp.write( f'/* Implementation of {classname} (wrapper for {structname}). */\n')
    out_cpp.write( '\n')

    # Trailing text in header, e.g. typedef for iterator.
    #
    if extras.class_top:
        # Strip leading blank line to avoid slightly odd-looking layout.
        text = clip( extras.class_top, '\n')
        text = textwrap.dedent( text)
        text = textwrap.indent( text, '    ')
        out_h.write( '\n')
        out_h.write( text)

    # Constructors
    #
    if constructor_fns:
        out_h.write( '\n')
        out_h.write( '    /* == Constructors. */\n')
    num_constructors = len(constructor_fns)
    for fnname, cursor, duplicate_type in constructor_fns:
        # clang-6 appears not to be able to handle fn args that are themselves
        # function pointers, so for now we allow make_function_wrapper() to
        # fail, so we need to use temporary buffers, otherwise out_functions_h
        # and out_functions_cpp can get partial text written.
        #
        temp_out_h = io.StringIO()
        temp_out_cpp = io.StringIO()
        try:
            class_write_method(
                    tu,
                    register_fn_use,
                    structname,
                    classname,
                    fnname,
                    temp_out_h,
                    temp_out_cpp,
                    static=False,
                    constructor=True,
                    extras=extras,
                    struct=struct,
                    duplicate_type=duplicate_type,
                    )
        except Clang6FnArgsBug as e:
            log( 'Unable to wrap function {fnname} becase: {e}')
        else:
            out_h.write( temp_out_h.getvalue())
            out_cpp.write( temp_out_cpp.getvalue())

    # Custom constructors.
    #
    for extra_constructor in extras.constructors_extra:
        class_custom_method(
                register_fn_use,
                classname,
                extra_constructor,
                out_h,
                out_cpp,
                )
        num_constructors += 1

    # Look for function that can be used by copy constructor and operator=.
    #
    if not extras.pod and extras.copyable and extras.copyable != 'default':
        class_copy_constructor(
                tu,
                register_fn_use,
                structname,
                base_name,
                classname,
                constructor_fns,
                out_h,
                out_cpp,
                )

    # Auto-add all methods that take <structname> as first param.
    #
    for fnname in find_wrappable_function_with_arg0_type( tu, structname):
        if fnname in extras.method_wrappers:
            #log( 'auto-detected fn already in {structname} method_wrappers: {fnname}')
            pass
        elif fnname.startswith( 'fz_new_draw_device'):
            # fz_new_draw_device*() functions take first arg fz_matrix, but
            # aren't really appropriate for the fz_matrix wrapper class.
            #
            pass
        else:
            for extramethod in extras.methods_extra:
                if extramethod.name_args.startswith( f'{clip(fnname, "fz_", "_s")}('):
                    #log( 'fnname already in extras.methods_extra: {extramethod.name_args}')
                    break
            else:
                #log( 'adding to extras.method_wrappers: {fnname}')
                extras.method_wrappers.append( fnname)


    # Extra static methods.
    #
    if extras.method_wrappers_static:
        out_h.write( '\n')
        out_h.write( '    /* == Static methods. */\n')
    for fnname in extras.method_wrappers_static:
        class_write_method(
                tu,
                register_fn_use,
                structname,
                classname,
                fnname,
                out_h,
                out_cpp,
                static=True,
                struct=struct,
                )

    # Extra methods that wrap fz_*() fns.
    #
    if extras.method_wrappers or extras.methods_extra:
        out_h.write( '\n')
        out_h.write( '    /* == Methods. */')
        out_h.write( '\n')
    extras.method_wrappers.sort()
    for fnname in extras.method_wrappers:
        class_write_method(
                tu,
                register_fn_use,
                structname,
                classname,
                fnname,
                out_h,
                out_cpp,
                struct=struct,
                )

    # Custom methods.
    #
    is_container = 0
    custom_destructor = False
    for extramethod in extras.methods_extra:
        is_constructor, is_destructor, is_begin_end = class_custom_method(
                register_fn_use,
                classname,
                extramethod,
                out_h,
                out_cpp,
                )
        if is_constructor:
            num_constructors += 1
        if is_destructor:
            custom_destructor = True
        if is_begin_end:
            is_container += 1

    assert is_container==0 or is_container==2   # Should be begin()+end() or neither.
    if is_container:
        pass
        #jlib.log( 'Generated class has begin() and end(): {classname=}')

    if num_constructors == 0 or extras.constructor_raw:
        class_raw_constructor(
                register_fn_use,
                classname,
                struct,
                structname,
                base_name,
                extras,
                constructor_fns,
                out_h,
                out_cpp,
                )

    # Accessor methods to POD data.
    #
    if extras.accessors and extras.pod == 'inline':
        log( 'ignoring {extras.accessors=} for {structname=} because {extras.pod=}.')
    elif extras.accessors:
        out_h.write( f'\n')
        out_h.write( f'    /* == Accessors to members of {structname} m_internal. */\n')
        out_h.write( '\n')
        class_accessors(
                tu,
                register_fn_use,
                classname,
                struct,
                structname,
                extras,
                out_h,
                out_cpp,
                )

    # Destructor.
    #
    if not custom_destructor:
        out_h.write( f'\n')
        class_destructor(
                register_fn_use,
                classname,
                destructor_fns,
                out_h,
                out_cpp,
                )

    # Class members.
    #
    out_h.write( '\n')
    out_h.write( '    /* == Member data. */\n')
    out_h.write( '\n')
    if extras.pod == 'inline':
        out_h.write( f'    /* These members are the same as the members of {structname}. */\n')
        for c in struct.type.get_canonical().get_fields():
            out_h.write( f'    {declaration_text(c.type, c.spelling)};\n')
    elif extras.pod:
        out_h.write( f'    {struct.spelling}  m_internal; /* Wrapped data is held by value. */\n')
    else:
        out_h.write( f'    {structname}* m_internal; /* Pointer to wrapped data. */\n')

    # Trailing text in header, e.g. typedef for iterator.
    #
    if extras.class_bottom:
        out_h.write( textwrap.indent( textwrap.dedent( extras.class_bottom), '    '))

    # Private copy constructor if not copyable.
    #
    if not extras.copyable:
        out_h.write(  '\n')
        out_h.write(  '    private:\n')
        out_h.write(  '\n')
        out_h.write(  '    /* This class is not copyable or assignable. */\n')
        out_h.write( f'    {classname}(const {classname}& rhs);\n')
        out_h.write( f'    {classname}& operator=(const {classname}& rhs);\n')

    # Class definition end.
    #
    out_h.write( '};\n')

    if extras.class_post:
        out_h_end.write( textwrap.dedent( extras.class_post))

    if extras.extra_cpp:
        out_cpp.write( textwrap.dedent( extras.extra_cpp))

    return is_container


def header_guard( name, out):
    '''
    Writes header guard for <name> to stream <out>.
    '''
    m = ''
    for c in name:
        m += c.upper() if c.isalnum() else '_'
    out.write( f'#ifndef {m}\n')
    out.write( f'#define {m}\n')
    out.write( '\n')

def tabify( filename, text):
    '''
    Returns <text> with leading multiples of 4 spaces converted to tab
    characters.
    '''
    ret = ''
    linenum = 0
    for line in text.split( '\n'):
        linenum += 1
        i = 0
        while 1:
            if i == len(line):
                break
            if line[i] != ' ':
                break
            i += 1
        if i % 4:
            if line[ int(i/4)*4:].startswith( ' *'):
                # This can happen in comments.
                pass
            else:
                log( '*** {filename}:{linenum}: {i=} {line!r=} indentation is not a multiple of 4')
        num_tabs = int(i / 4)
        ret += num_tabs * '\t' + line[ num_tabs*4:] + '\n'

    return ret


def cpp_source( dir_mupdf, namespace, base, header_git, doit=True):
    '''
    Generates all .h and .cpp files.

    dir_mupdf:
        Location of mupdf checkout.
    namespace:
        C++ namespace to use.
    base:
        Directory in which all generated files are placed.
    doit:
        For debugging only. If false, we don't actually write to any files.

    Returns (tu, hs, cpps, fn_usage_filename, container_classnames, fn_usage).
        tu:
            From clang.
        hs:
            List of generated .h files.
        cpps:
            List of generated .cpp files.
        fn_usage_filename:
            Name of output file containing information on function usage.
        container_classnames:
            List of generated classnames that have begin() and end() methods.
        fn_usage:
            Dict mapping fz_* function names to number of usages in generated
            class code.
            Name of output file containing information on function usage.
    '''
    assert dir_mupdf.endswith( '/')
    assert base.endswith( '/')
    index = clang.cindex.Index.create()
    #log( '{dir_mupdf=} {base=}')

    header = f'{dir_mupdf}include/mupdf/fitz.h'
    assert os.path.isfile( header), f'header={header}'

    # Get clang to parse mupdf/fitz.h and mupdf/pdf.h.
    #
    # It might be possible to use index.parse()'s <unsaved_files> arg to
    # specify these multiple files, but i couldn't get that to work.
    #
    # So instead we write some #include's to a temporary file and ask clang to
    # parse it.
    #
    temp_h = f'_mupdfwrap_temp.h'
    try:
        with open( temp_h, 'w') as f:
            f.write( '#include "mupdf/fitz.h"\n')
            f.write( '#include "mupdf/pdf.h"\n')
        tu = index.parse(
                    temp_h,
                    args=(
                            '-I', f'{dir_mupdf}include',
                            '-I', g_clang_info.include_path,
                            ),
                    )
    finally:
        if os.path.isfile( temp_h):
            os.remove( temp_h)

    os.makedirs( f'{base}include/mupdf', exist_ok=True)
    os.makedirs( f'{base}implementation', exist_ok=True)

    if doit:
        class File:
            def __init__( self, filename, tabify=True):
                self.filename = filename
                self.tabify = tabify
                self.file = io.StringIO()
                self.line_begin = True
            def write( self, text, fileline=False):
                if fileline:
                    # Generate #line <line> "<filename>" for our caller's
                    # location. This makes any compiler warnings refer to thei
                    # python code rather than the generated C++ code.
                    tb = traceback.extract_stack( None)
                    filename, line, function, source = tb[0]
                    if self.line_begin:
                        self.file.write( f'#line {line} "{filename}"\n')
                self.file.write( text)
                self.line_begin = text.endswith( '\n')
            def close( self):
                if self.filename:
                    # Overwrite if contents differ.
                    text = self.get()
                    if self.tabify:
                        text = tabify( self.filename, text)
                    jlib.update_file( text, self.filename)
            def get( self):
                return self.file.getvalue()
    else:
        class File:
            def __init__( self, filename):
                pass
            def write( self, text, fileline=False):
                pass
            def close( self):
                pass

    class Outputs:
        '''
        A set of output files.

        For convenience, after outputs.add( 'foo', 'foo.c'), outputs.foo is a
        python stream that writes to 'foo.c'.
        '''
        def __init__( self):
            self.items = []

        def add( self, name, filename):
            '''
            Sets self.<name> to file opened for writing on <filename>.
            '''
            file = File( filename)
            self.items.append( (name, filename, file))
            setattr( self, name, file)

        def get( self):
            '''
            Returns list of (name, filename, file) tuples.
            '''
            return self.items

        def close( self):
            for name, filename, file in self.items:
                file.close()

    out_cpps = Outputs()
    out_hs = Outputs()
    for name in (
            'classes',
            'exceptions',
            'functions',
            'internal',
            ):
        out_hs.add( name, f'{base}include/mupdf/{name}.h')
        out_cpps.add( name, f'{base}implementation/{name}.cpp')

    # Create extra File that writes to internal buffer rather than an actual
    # file, which we will append to out_h.
    #
    out_h_classes_end = File( None)

    # Make text of header comment for all generated file.
    #
    header_text = textwrap.dedent(
            f'''
            /*
            This file was auto-generated by mupdfwrap.py.
            ''')

    if header_git:
        git_id = jlib.get_git_id( dir_mupdf, allow_none=True)
        if git_id:
            git_id = git_id.split('\n', 1)
            header_text += textwrap.dedent(
                    f'''
                    mupdf checkout:
                        {git_id[0]}'
                    ''')

    header_text += '*/\n'
    header_text += '\n'
    header_text = header_text[1:]   # Strip leading \n.
    for _, _, file in out_cpps.get() + out_hs.get():
        file.write( header_text)

    # Write multiple-inclusion guards into headers:
    #
    for name, filename, file in out_hs.get():
        prefix = f'{base}include/'
        assert filename.startswith( prefix)
        name = filename[ len(prefix):]
        header_guard( name, file)

    # Write required #includes into .h files:
    #
    out_hs.exceptions.write( textwrap.dedent(
            '''
            #include <stdexcept>
            #include <string>

            #include "mupdf/fitz.h"


            '''))

    out_hs.functions.write( textwrap.dedent(
            '''
            #include "mupdf/fitz.h"
            #include "mupdf/pdf.h"

            #ifdef SWIG
                #define mupdf_OUTPARAM(name)  OUTPUT
            #else
                #define mupdf_OUTPARAM(name)  name
            #endif


            '''))

    out_hs.classes.write( textwrap.dedent(
            '''
            #include "mupdf/fitz.h"
            #include "mupdf/functions.h"
            #include "mupdf/pdf.h"

            #include <string>
            #include <vector>

            #ifdef SWIG
                #define mupdf_OUTPARAM(name)  OUTPUT
            #else
                #define mupdf_OUTPARAM(name)  name
            #endif


            '''))

    # Write required #includes into .cpp files:
    #
    out_cpps.exceptions.write( textwrap.dedent(
            '''
            #include "mupdf/exceptions.h"
            #include "mupdf/fitz.h"


            '''))

    out_cpps.functions.write( textwrap.dedent(
            '''
            #include "mupdf/exceptions.h"
            #include "mupdf/functions.h"
            #include "mupdf/internal.h"


            '''))

    out_cpps.classes.write(
            textwrap.dedent(
            '''
            #include "mupdf/classes.h"
            #include "mupdf/exceptions.h"
            #include "mupdf/internal.h"

            #include "mupdf/fitz/geometry.h"

            #include <sstream>
            #include <string.h>


            '''))

    namespace = 'mupdf'
    for _, _, file in out_cpps.get() + out_hs.get():
        if file == out_cpps.internal:
            continue
        make_namespace_open( namespace, file)

    # Write source code for exceptions and wrapper functions.
    #
    log( 'Creating wrapper functions...')
    make_function_wrappers(
            tu,
            namespace,
            out_hs.exceptions,
            out_cpps.exceptions,
            out_hs.functions,
            out_cpps.functions,
            out_hs.internal,
            out_cpps.internal,
            )

    fn_usage = dict()
    functions_unrecognised = set()
    for fnname, cursor in find_functions_starting_with( tu, ('fz_', 'pdf_'), method=True):
        fn_usage[ fnname] = [0, cursor]

    def register_fn_use( name):
        assert name.startswith( ('fz_', 'pdf_'))
        if name in fn_usage:
            fn_usage[ name][0] += 1
        else:
            functions_unrecognised.add( name)

    # Write source code for wrapper classes.
    #
    log( 'Creating wrapper classes...')

    if 0: out_hs.classes.write( textwrap.dedent(
            f'''
            typedef fz_separation_behavior {rename.class_( 'fz_separation_behavior')};

            '''))

    # Find all classes that we can create.
    #
    classes = []
    for cursor in tu.cursor.get_children():
        if not cursor.spelling.startswith( ('fz_', 'pdf_')):
            continue
        if cursor.kind != clang.cindex.CursorKind.TYPEDEF_DECL:
            continue;
        type_ = cursor.underlying_typedef_type.get_canonical()
        if type_.kind != clang.cindex.TypeKind.RECORD:
            continue

        if not cursor.is_definition():
            extras = classextras.get( cursor.spelling)
            if extras and extras.opaque:
                pass
                #log( 'Creating wrapper for opaque struct: {cursor.spelling=}')
            else:
                continue

        structname = type_.spelling
        structname = clip( structname, 'struct ')
        if omit_class( structname):
            continue
        if structname in omit_class_names0:
            continue
        classname = rename.class_( structname)
        #log( 'Creating class wrapper. {classname=} {cursor.spelling=} {structname=}')

        # For some reason after updating mupdf 2020-04-13, clang-python is
        # returning two locations for struct fz_buffer_s, both STRUCT_DECL. One
        # is 'typedef struct fz_buffer_s fz_buffer;', the other is the full
        # struct definition.
        #
        # No idea why this is happening. Using .canonical doesn't seem to
        # affect things.
        #
        for cl, cu, s in classes:
            if cl == classname:
                log( 'ignoring duplicate STRUCT_DECL for {structname=}')
                break
        else:
            classes.append( (classname, cursor, structname))

    classes.sort()

    if omit_class_names:
        log( '*** Some names to omit were left over: {omit_class_names=}')
        #assert 0

    # Write forward declarations - this is required because some class
    # methods take pointers to other classes.
    #
    out_hs.classes.write( '/* Forward declarations of all classes that we define. */\n')
    for classname, struct, structname in classes:
        out_hs.classes.write( f'struct {classname};\n')
    out_hs.classes.write( '\n')

    container_classnames = []

    # Create each class.
    #
    for classname, struct, structname in classes:
        #log( 'creating wrapper {classname} for {cursor.spelling}')
        with jlib.LogPrefixScope( f'{structname}: '):
            is_container = class_wrapper(
                    tu,
                    register_fn_use,
                    struct,
                    structname,
                    classname,
                    out_hs.classes,
                    out_cpps.classes,
                    out_h_classes_end,
                    )
        if is_container:
            container_classnames.append( classname)

    # Write close of namespace.
    out_hs.classes.write( out_h_classes_end.get())
    for _, _, file in out_cpps.get() + out_hs.get():
        if file == out_cpps.internal:
            continue
        make_namespace_close( namespace, file)


    # Terminate multiple-inclusion guards in headers:
    #
    for _, _, file in out_hs.get():
        file.write( '\n#endif\n')

    out_hs.close()
    out_cpps.close()

    filenames_h = [filename for _, filename, _ in out_hs.get()]
    filenames_cpp = [filename for _, filename, _ in out_cpps.get()]
    if 0:
        log( 'Have created:')
        for filename in filenames_h + filenames_cpp:
            log( '    {filename}')

    fn_usage_filename = f'{base}fn_usage.txt'
    out_fn_usage = File( fn_usage_filename, tabify=False)
    functions_unused = 0
    functions_used = 0

    for fnname in sorted( fn_usage.keys()):
        n, cursor = fn_usage[ fnname]
        exclude_reasons = find_wrappable_function_with_arg0_type_excluded_cache.get( fnname, [])
        if n:
            functions_used += 1
        else:
            functions_unused += 1
        if n and not exclude_reasons:
            continue
        # 'cursor.displayname' doesn't include the return type, so we use our
        # declaration_text() fn to show the function details.
        #out_fn_usage.write( f'    {n}: {fnname}: {cursor.displayname}\n')
        #out_fn_usage.write( f'    {fnname}:\n')
        if 0:
            out_fn_usage.write( f'    {declaration_text( cursor.type, cursor.spelling, arg_names=1)}\n')
            out_fn_usage.write( f'        {n}\n')

    out_fn_usage.write( f'Functions not wrapped by class methods:\n')
    out_fn_usage.write( '\n')

    for fnname in sorted( fn_usage.keys()):
        n, cursor = fn_usage[ fnname]
        exclude_reasons = find_wrappable_function_with_arg0_type_excluded_cache.get( fnname, [])
        if not exclude_reasons:
            continue
        if n:
            continue
        #if not exclude_reasons:
        #    out_fn_usage.write( f'    {declaration_text( cursor.type, cursor.spelling, arg_names=1)}\n')
        #    out_fn_usage.write( f'        *** not used, but no exclude reasons\n')
        #    out_fn_usage.write( '\n')
        #    continue
        num_interesting_reasons = 0
        for t, description in exclude_reasons:
            if t == MethodExcludeReason_FIRST_ARG_NOT_STRUCT:
                continue
            if t == MethodExcludeReason_VARIADIC:
                continue
            num_interesting_reasons += 1
        if num_interesting_reasons:
            try:
                out_fn_usage.write( f'    {declaration_text( cursor.type, cursor.spelling, arg_names=1)}\n')
            except Clang6FnArgsBug as e:
                out_fn_usage.write( f'    {cursor.spelling} [full prototype not available due to known clang-6 issue]\n')
            #out_fn_usage.write( f'        exclude reasons:\n')
            for t, description in exclude_reasons:
                if t == MethodExcludeReason_FIRST_ARG_NOT_STRUCT:
                    continue
                out_fn_usage.write( f'        {description}\n')
            out_fn_usage.write( '\n')

    out_fn_usage.write( f'\n')
    out_fn_usage.write( f'Functions used more than once:\n')
    for fnname in sorted( fn_usage.keys()):
        n, cursor = fn_usage[ fnname]
        if n > 1:
            out_fn_usage.write( f'    n={n}: {declaration_text( cursor.type, cursor.spelling, arg_names=1)}\n')

    out_fn_usage.write( f'\n')
    out_fn_usage.write( f'Number of wrapped functions: {len(fn_usage)}\n')
    out_fn_usage.write( f'Number of wrapped functions used by wrapper classes: {functions_used}\n')
    out_fn_usage.write( f'Number of wrapped functions not used by wrapper classes: {functions_unused}\n')

    out_fn_usage.close()

    return tu, base, filenames_h, filenames_cpp, fn_usage_filename, container_classnames, fn_usage


def compare_fz_usage(
        tu,
        directory,
        fn_usage,
        ):
    '''
    Looks for fz_ items in git files within <directory> and compares to what
    functions we have wrapped in <fn_usage>.
    '''

    filenames = jlib.system( f'cd {directory}; git ls-files .', out='return')

    class FzItem:
        def __init__( self, type_, uses_structs=None):
            self.type_ = type_
            if self.type_ == 'function':
                self.uses_structs = uses_structs

    # Set fz_items to map name to info about function/struct.
    #
    fz_items = dict()
    for cursor in tu.cursor.get_children():
        name = cursor.mangled_name
        if not name.startswith( ('fz_', 'pdf_')):
            continue
        uses_structs = False
        if (1
                and name.startswith( ('fz_', 'pdf_'))
                and cursor.kind == clang.cindex.CursorKind.FUNCTION_DECL
                and (
                    cursor.linkage == clang.cindex.LinkageKind.EXTERNAL
                    or
                    cursor.is_definition()  # Picks up static inline functions.
                    )
                ):
            def uses_struct( type_):
                '''
                Returns true if <type_> is a fz struct or pointer to fz struct.
                '''
                if type_.kind == clang.cindex.TypeKind.POINTER:
                    type_ = type_.get_pointee()
                type_ = type_.get_canonical()
                if type_.spelling.startswith( 'struct fz_'):
                    return True
            # Set uses_structs to true if fn returns a fz struct or any
            # argument is a fz struct.
            if uses_struct( cursor.result_type):
                uses_structs = True
            else:
                for arg, arg_name, separator, alt, out_param in get_args( tu, cursor):
                    if uses_struct( arg.type):
                        uses_structs = True
                        break
            if uses_structs:
                pass
                #log( 'adding function {name=} {uses_structs=}')
            fz_items[ name] = FzItem( 'function', uses_structs)

    directory_names = dict()
    for filename in filenames.split( '\n'):
        if not filename:
            continue
        path = os.path.join( directory, filename)
        log( '{filename!r=} {path=}')
        with open( path, 'r', encoding='utf-8', errors='replace') as f:
            text = f.read()
        for m in re.finditer( '(fz_[a-z0-9_]+)', text):

            name = m.group(1)
            info = fz_items.get( name)
            if info:
                if (0
                        or (info.type_ == 'function' and info.uses_structs)
                        or (info.type_ == 'fz-struct')
                        ):
                    directory_names.setdefault( name, 0)
                    directory_names[ name] += 1

    name_max_len = 0
    for name, n in sorted( directory_names.items()):
        name_max_len = max( name_max_len, len( name))

    n_missing = 0
    fnnames = sorted( fn_usage.keys())
    for fnname in fnnames:
        classes_n, cursor = fn_usage[ fnname]
        directory_n = directory_names.get( name, 0)
        if classes_n==0 and directory_n:
            n_missing += 1
            log( '    {fnname:40} {classes_n=} {directory_n=}')

    log( '{n_missing}')



def build_swig( build_dirs, container_classnames, language='python', swig='swig'):
    '''
    Builds python wrapper for all mupdf_* functions and classes.
    '''

    # Find version of swig.
    t = jlib.system( f'{swig} -version', out='return')
    m = re.search( 'SWIG Version ([0-9]+)[.]([0-9]+)[.]([0-9]+)', t)
    assert m
    swig_major = int( m.group(1))

    # Create a .i file for SWIG.
    #
    text = textwrap.dedent(f'''
            %ignore fz_append_vprintf;
            %ignore fz_error_stack_slot;
            %ignore fz_format_string;
            %ignore fz_vsnprintf;
            %ignore fz_vthrow;
            %ignore fz_vwarn;
            %ignore fz_write_vprintf;

            // Not implemented in mupdf.so: fz_colorspace_name_process_colorants
            %ignore fz_colorspace_name_process_colorants;

            %ignore fz_set_stderr;
            %ignore fz_set_stdout;
            %ignore fz_open_file_w;

            %ignore {rename.function('fz_append_vprintf')};
            %ignore {rename.function('fz_error_stack_slot_s')};
            %ignore {rename.function('fz_format_string')};
            %ignore {rename.function('fz_vsnprintf')};
            %ignore {rename.function('fz_vthrow')};
            %ignore {rename.function('fz_vwarn')};
            %ignore {rename.function('fz_write_vprintf')};
            %ignore {rename.function('fz_set_stderr')};
            %ignore {rename.function('fz_set_stdout')};
            %ignore {rename.function('fz_open_file_w')};

            %ignore {rename.function('fz_vsnprintf')};
            %ignore {rename.function('fz_vthrow')};
            %ignore {rename.function('fz_vwarn')};
            %ignore {rename.function('fz_append_vprintf')};
            %ignore {rename.function('fz_write_vprintf')};
            %ignore {rename.function('fz_format_string')};
            %ignore {rename.function('fz_open_file_w')};

            // Might prefer to #include mupdf/exceptions.h and make the
            // %exception block below handle all the different exception types,
            // but swig-3 cannot parse 'throw()' in mupdf/exceptions.h.
            //
            // So for now we just #include <stdexcept> and handle
            // std::exception only.

            %{{
            #include <stdexcept>
            #include "mupdf/functions.h"

            #include "mupdf/classes.h"
            %}}

            %include exception.i
            %include std_string.i
            %include carrays.i
            %include std_vector.i
            %include argcargv.i

            namespace std
            {{
                %template(vectori) vector<int>;
            }};

            // Make sure that operator++() gets converted to __next__().
            //
            // Note that swig already seems to do:
            //
            //     operator* => __ref__
            //     operator== => __eq__
            //     operator!= => __ne__
            //     operator-> => __deref__
            //
            // Just need to add this method to containers that already have
            // begin() and end():
            //     def __iter__( self):
            //         return CppIterator( self)
            //

            %rename(__increment__) *::operator++;


            %array_functions(unsigned char, bytes);

            %exception {{
              try {{
                $action
              }}
              catch(std::exception& e) {{
                SWIG_exception(SWIG_RuntimeError, e.what());
              }}
              catch(...) {{
                SWIG_exception(SWIG_RuntimeError, "Unknown exception");
              }}
            }}

            // Ensure SWIG handles OUTPUT params.
            //
            %include "cpointer.i"

            // Don't wrap raw fz_*() functions.
            %rename("$ignore", regexmatch$name="^fz_", %$isfunction, %$not %$ismember) "";

            {'%feature("autodoc", "3");' if swig_major < 4 else ''}

            // Get swig about pdf_clean_file()'s (int,argv)-style args:
            %apply (int ARGC, char **ARGV) {{ (int retainlen, char *retainlist[]) }}

            #include "mupdf/functions.h"
            #include "mupdf/classes.h"

            %pointer_functions(int, pint);
            %pointer_functions(fz_font, pfont);

            ''')

    # Make some additions to the generated Python module.
    #
    # E.g. python wrappers for functions that take out-params should return
    # tuples.
    #
    text += textwrap.dedent('''
            %pythoncode %{

            import re

            # Wrap parse_page_range() to fix SWIG bug where a NULL return value
            # is not included in the returned list. This occurs with SWIG-3.0;
            # maybe fixed in SWIG-4?
            #
            w_parse_page_range = parse_page_range
            def parse_page_range(s, n):
                ret = w_parse_page_range(s, n)
                if len(ret) == 2:
                    return None, ret[0], ret[1]
                else:
                    return ret[0], ret[1], ret[2]

            # Provide native python implementation of format_output_path() (->
            # fz_format_output_path).
            #
            def format_output_path( format, page):
                m = re.search( '(%[0-9]*d)', format)
                if m:
                    ret = format[ :m.start(1)] + str(page) + format[ m.end(1):]
                else:
                    dot = format.rfind( '.')
                    if dot < 0:
                        dot = len( format)
                    ret = format[:dot] + str(page) + format[dot:]
                return ret

            class IteratorWrap:
                """
                This is a Python iterator for containers that have C++-style
                begin() and end() methods that return iterators.

                Iterators must have the following methods:

                    __increment__(): move to next item in the container.
                    __ref__(): return reference to item in the container.

                Must also be able to compare two iterators for equality.

                """
                def __init__( self, container):
                    self.container = container
                    self.pos = None
                    self.end = container.end()
                def __iter__( self):
                    return self
                def __next__( self):    # for python2.
                    if self.pos is None:
                        self.pos = self.container.begin()
                    else:
                        self.pos.__increment__()
                    if self.pos == self.end:
                        raise StopIteration()
                    return self.pos.__ref__()
                def next( self):    # for python3.
                    return self.__next__()

            ''')

    # Add __iter__() methods for all classes with begin() and end() methods.
    #
    for classname in container_classnames:
        text += f'{classname}.__iter__ = lambda self: IteratorWrap( self)\n'

    text += '%}\n'

    preprocess = False

    if 1:
        # This is a horrible hack to avoid swig failing because
        # include/mupdf/pdf/object.h defines an enum which contains a #include.
        #
        # Would like to pre-process files in advance so that swig doesn't see
        # the #include, but this breaks swig in a different way - swig cannot
        # cope with some code in system headers.
        #
        # So instead we copy include/mupdf/pdf/object.h into
        # {build_dirs.dir_mupdf}platform/python/include/mupdf/pdf/object.h,
        # manually expanding the #include using a simpe .replace() call. The
        # we specify {build_dirs.dir_mupdf}platform/python/include as the
        # first include path so that our modified mupdf/pdf/object.h will get
        # included in preference to the original.
        #
        jlib.system( f'mkdir -p {build_dirs.dir_mupdf}platform/python/include/mupdf/pdf')
        with open( f'{build_dirs.dir_mupdf}include/mupdf/pdf/object.h') as f:
            o = f.read()
        with open( f'{build_dirs.dir_mupdf}include/mupdf/pdf/name-table.h') as f:
            name_table_h = f.read()
        oo = o.replace( '#include "mupdf/pdf/name-table.h"\n', name_table_h)
        assert oo != o
        jlib.update_file( oo, f'{build_dirs.dir_mupdf}platform/python/include/mupdf/pdf/object.h')

    swig_i      = f'{build_dirs.dir_mupdf}platform/python/mupdfcpp_swig.i'
    include1    = f'{build_dirs.dir_mupdf}include/'
    include2    = f'{build_dirs.dir_mupdf}platform/c++/include'
    include3    = None
    if preprocess:
        include3    = f'{build_dirs.dir_mupdf}platform/python/'
    swig_cpp    = f'{build_dirs.dir_mupdf}platform/python/mupdfcpp_swig.cpp'
    swig_py     = f'{build_dirs.dir_so}mupdf.py'

    os.makedirs( f'{build_dirs.dir_mupdf}platform/python', exist_ok=True)
    jlib.update_file( text, swig_i)

    command = (
            textwrap.dedent(
            f'''
            {swig}
                -Wall
                -c++
                {" -doxygen" if swig_major >= 4 else ""}
                -{language}
                -module mupdf
                -outdir {build_dirs.dir_so}
                -o {swig_cpp}
                -includeall
                -I{build_dirs.dir_mupdf}platform/python/include
                -I{include1}
                -I{include2}
                -ignoremissing
                {swig_i}
            ''').strip().replace( '\n', ' \\\n')
            )

    jlib.build(
            (swig_i, include1, include2),
            (swig_cpp, swig_py),
            command,
            )


def build_swig_java( container_classnames):
    return build_swig( container_classnames, 'java')


def test_swig():
    '''
    For testing different swig .i constructs.
    '''
    test_i = textwrap.dedent('''
            %include argcargv.i

            %apply (int ARGC, char **ARGV) { (int retainlen, const char **retainlist) }
            %apply (int ARGC, char **ARGV) { (const char **retainlist, int retainlen) }
            %apply (int ARGC, char **ARGV) { (const char *retainlist[], int retainlen) }

            %clear double a, int ARGC, char **ARGV;
            %clear double a, int argc, char *argv[];
            %clear int ARGC, char **ARGV;
            %clear (double a, int ARGC, char **ARGV);
            %clear (double a, int argc, char *argv[]);
            %clear (int ARGC, char **ARGV);
            %clear int retainlen, const char **retainlist;

            int bar( int argc, char* argv[]);
            int foo( double a, int argc, char* argv[]);

            int qwe( double a, int argc, const char** argv);

            void ppdf_clean_file( char *infile, char *outfile, char *password, pdf_write_options *opts, int retainlen, const char **retainlist);
            void ppdf_clean_file2(char *infile, char *outfile, char *password, pdf_write_options *opts, const char **retainlist, int retainlen);
            void ppdf_clean_file3(char *infile, char *outfile, char *password, pdf_write_options *opts, const char *retainlist[], int retainlen);

            ''')
    jlib.update_file( test_i, 'test.i')

    jlib.system( textwrap.dedent(
            '''
            swig
                -Wall
                -c++
                -python
                -module test
                -outdir .
                -o test.cpp
                test.i
            ''').replace( '\n', ' \\\n')
            )



def test_mupdfcpp_swig():
    '''
    Test the python API.
    '''
    import mupdf
    import os
    import sys
    log = print

    log( 'have imported mupdf')

    if 1:
        # Test operations using functions:
        #
        log( 'Testing functions.')

        # Find mupdf:thirdparty/zlib/zlib.3.pdf, assuming we are in
        # mupdf:platform/python.
        zlib_pdf = os.path.abspath( f'{__file__}/../../../thirdparty/zlib/zlib.3.pdf')

        assert os.path.isfile( zlib_pdf), f'file does not exist: {zlib_pdf}'
        log( f'    Opening {zlib_pdf}')
        document = mupdf.open_document( zlib_pdf)
        log( f'    {mupdf.needs_password( document)}')
        log( f'    {mupdf.needs_password( document)}')
        log( f'    {mupdf.count_pages( document)}')
        log( f'    {mupdf.document_output_intent( document)}')

    # Test operations using classes:
    #
    log( f'Testing classes')
    for filename in (
            os.path.expanduser( '~/pdf_reference17.pdf'),
            os.path.expanduser( '~/artifex/testfiles/pdf_reference17.pdf'),
            ):
        if os.path.isfile( filename):
            break
    else:
        raise Exception( 'cannot find pdf_reference17.pdf')

    document = mupdf.Document( filename)
    log( f'Have create document for {filename}')
    log( f'{document.needs_password()}')
    log( f'{document.count_pages()}')

    zoom = 10
    scale = mupdf.Matrix.scale( zoom/100., zoom/100.)
    page_number = 0
    log( f'Have created scale: a={scale.a} b={scale.b} c={scale.c} d={scale.d} e={scale.e} f={scale.f}')

    colorspace = mupdf.Colorspace( mupdf.Colorspace.Fixed_RGB)
    log( f'{colorspace.m_internal.key_storable.storable.refs}')
    pixmap = mupdf.Pixmap( document, page_number, scale, colorspace, 0)
    log( f'Have created pixmap: {pixmap.m_internal.w} {pixmap.m_internal.h} {pixmap.m_internal.stride} {pixmap.m_internal.n}')

    filename = 'mupdf_test-out.png'
    pixmap.save_pixmap_as_png( filename)
    log( f'Have created {filename} using pixmap.save_pixmap_as_png().')


    # Print image data in ascii PPM format. Copied from
    # mupdf/docs/examples/example.c.
    #
    samples = pixmap.m_internal.samples
    stride = pixmap.m_internal.stride
    n = pixmap.m_internal.n
    filename = 'mupdf_test-out.ppm'
    with open( filename, 'w') as f:
        f.write( 'P3\n')
        f.write( '%s %s\n' % (pixmap.m_internal.w, pixmap.m_internal.h))
        f.write( '255\n')
        for y in range( 0, pixmap.m_internal.h):
            for x in range( pixmap.m_internal.w):
                if x:
                    f.write( '  ')
                offset = y * stride + x * n
                f.write( '%3d %3d %3d' % (
                        mupdf.bytes_getitem( samples, offset + 0),
                        mupdf.bytes_getitem( samples, offset + 1),
                        mupdf.bytes_getitem( samples, offset + 2),
                        ))
            f.write( '\n')
    log( f'Have created {filename} by scanning pixmap.')

    # Generate .png and but create Pixmap from Page instead of from Document.
    #
    page = mupdf.Page(document, 0)
    separations = page.page_separations()
    log( f'page_separations() returned {"true" if separations else "false"}')
    pixmap = mupdf.Pixmap( page, scale, colorspace, 0)
    filename = 'mupdf_test-out2.png'
    pixmap.save_pixmap_as_png( filename)
    log( f'Have created {filename} using pixmap.save_pixmap_as_png()')

    # Show links
    log( f'Links.')
    page = mupdf.Page(document, 0)
    link = mupdf.load_links( page.m_internal);
    log( f'{link}')
    if link:
        for i in link:
            log( f'{i}')

    # Check we can iterate over Link's, by creating one manually.
    #
    link = mupdf.Link( mupdf.Rect(0, 0, 1, 1), None, "hello")
    log( f'items in <link> are:')
    for i in link:
        log( f'    {i.m_internal.refs} {i.m_internal.uri}')

    # Check iteration over Outlines.
    #
    log( f'Outlines.')
    outline = mupdf.Outline( document)
    log( f'{outline.uri()} {outline.page()} {outline.x()} {outline.y()} {outline.is_open()} {outline.title()}')
    log( f'items in outline tree are:')
    for o in outline:
        log( f'    {o.uri()} {o.page()} {o.x()} {o.y()} {o.is_open()} {o.title()}')

    # Check iteration over StextPage.
    #
    log( f'StextPage.')
    stext_options = mupdf.StextOptions(0)
    stext_page = mupdf.StextPage( document, 40, stext_options)
    device_stext = mupdf.Device( stext_page, stext_options)
    matrix = mupdf.Matrix()
    page = mupdf.Page( document, 0)
    cookie = mupdf.Cookie()
    page.run( device_stext, matrix, cookie)
    log( f'    stext_page is:')
    for block in stext_page:
        log( f'        block:')
        for line in block:
            line_text = ''
            for char in line:
                line_text += chr( char.m_internal.c)
            log( f'            {line_text}')

    device_stext.close_device()

    # Check copy-constructor.
    log( f'Checking copy-constructor')
    document2 = mupdf.Document( document)
    del document
    page = mupdf.Page(document2, 0)
    scale = mupdf.Matrix()
    pixmap = mupdf.Pixmap( page, scale, colorspace, 0)
    pixmap.save_pixmap_as_png( 'mupdf_test-out3.png')

    stdout = mupdf.Output(mupdf.Output.Fixed_STDOUT)
    log( f'{type(stdout)} {stdout.m_internal.state}')

    mediabox = page.bound_page()
    out = mupdf.DocumentWriter( filename, 'png', '')
    dev = out.begin_page( mediabox)
    page.run( dev, mupdf.Matrix(mupdf.fz_identity), mupdf.Cookie())
    out.end_page()

    # Check out-params are converted into python return value.
    bitmap = mupdf.Bitmap( 10, 20, 8, 72, 72)
    bitmap_details = bitmap.bitmap_details()
    log( f'{bitmap_details}')
    assert bitmap_details == [10, 20, 8, 12]

    log( f'finished')


class BuildDirs:
    '''
    Locations of various generated files.
    '''
    def __init__( self):

        # Assume we are in mupdf/scripts/.
        file_ = os.path.abspath( __file__)
        assert file_.endswith( '/scripts/mupdfwrap.py'), '__file__=%s file_=%s' % (__file__, file_)
        dir_mupdf = os.path.abspath( f'{file_}/../../')
        if not dir_mupdf.endswith( '/'):
            dir_mupdf += '/'

        # Directories used with --build.
        self.dir_mupdf      = dir_mupdf

        # Directory used with --ref.
        self.ref_dir        = os.path.abspath( f'{self.dir_mupdf}mupdfwrap_ref/')
        if not self.ref_dir.endswith( '/'):
            self.ref_dir += '/'

        # Default to release build.
        self.set_dir_so( f'{self.dir_mupdf}build/shared-release/')

    def set_dir_so( self, dir_so):
        dir_so = os.path.abspath( dir_so)
        if not dir_so.endswith( '/'):
            dir_so += '/'
        self.dir_so = dir_so

        # Compile flags that we use to build libmupdfcpp.so depend on the flags
        # used to build libmupdf.so - mupdf code is different depending on
        # whether NDEBUG is defined.
        #
        if 0: pass
        elif dir_so == f'{self.dir_mupdf}build/shared-debug/':
            self.cpp_flags = '-g'
        elif dir_so == f'{self.dir_mupdf}build/shared-release/':
            self.cpp_flags = '-O2 -DNDEBUG'
        elif dir_so == f'{self.dir_mupdf}build/shared-memento/':
            self.cpp_flags = '-g -DMEMENTO'
        else:
            self.cpp_flags = None
            log( 'Warning: unrecognised {dir_so=}, so cannot determine cpp_flags')



def main():

    # Set up behaviour of jlib.log().
    #
    jlib.g_log_prefixes.append( jlib.LogPrefixTime( elapsed=True))
    jlib.g_log_prefixes.append( jlib.g_log_prefixe_scopes)
    jlib.g_log_prefixes.append( jlib.LogPrefixFileLine())

    build_dirs = BuildDirs()
    swig = 'swig'

    args = jlib.Args( sys.argv[1:])
    while 1:
        try:
            arg = args.next()
        except StopIteration:
            break
        #log( 'Handling {arg=}')

        with jlib.LogPrefixScope( f'{arg}: '):
            if 0:
                pass

            elif arg == '--build' or arg == '-b':
                h_files     = []
                cpp_files   = []
                container_classnames = None
                force_rebuild = False
                header_git = False

                while 1:
                    actions = args.next()
                    if 0:
                        pass
                    elif actions == '-f':
                        force_rebuild = True
                    elif actions.startswith( '-'):
                        raise Exception( f'Unrecognised --build flag: {actions}')
                    else:
                        break

                if actions == 'all':
                    actions = 'm0123'

                for action in actions:
                    with jlib.LogPrefixScope( f'{action}: '):
                        jlib.log( '{action=}')
                        if 0:
                            pass

                        elif action == 'm':
                            # Build libmupdf.so.
                            log( '{build_dirs.dir_mupdf=}')

                            command = f'cd {build_dirs.dir_mupdf} && make HAVE_GLUT=no shared=yes verbose=yes'
                            #command += ' USE_SYSTEM_FREETYPE=yes USE_SYSTEM_ZLIB=yes'
                            if 0: pass
                            elif build_dirs.dir_so == f'{build_dirs.dir_mupdf}build/shared-debug/':
                                command += ' build=debug'
                            elif build_dirs.dir_so == f'{build_dirs.dir_mupdf}build/shared-release/':
                                command += ' build=release'
                            elif build_dirs.dir_so == f'{build_dirs.dir_mupdf}build/shared-memento/':
                                command += ' build=memento'
                            else:
                                raise Exception( f'Unrecognised dir_so={build_dirs.dir_so}')

                            jlib.system( command, prefix=jlib.log_text())


                        elif action == '0':
                            # Generate C++ code that wraps the fz_* API.
                            namespace = 'mupdf'
                            tu, base, hs, cpps, fn_usage_filename, container_classnames, fn_usage = cpp_source(
                                    build_dirs.dir_mupdf,
                                    namespace,
                                    f'{build_dirs.dir_mupdf}platform/c++/',
                                    header_git,
                                    )

                            h_files += hs
                            cpp_files += cpps

                            for dir_ in (
                                    f'{build_dirs.dir_mupdf}platform/c++/implementation/',
                                    f'{build_dirs.dir_mupdf}platform/c++/include/', '.h',
                                    ):
                                for path in jlib.get_filenames( dir_):
                                    _, ext = os.path.splitext( path)
                                    if ext not in ('.h', '.cpp'):
                                        continue
                                    if path in h_files + cpp_files:
                                        continue
                                    log( 'Removing unknown C++ file: {path}')
                                    os.remove( path)

                            jlib.log( 'Wrapper classes that are containers: {container_classnames=}')

                            # Output info about fz_*() functions that we don't make use
                            # of in class methods.
                            #
                            # This is superceded by automatically finding fuctions to wrap.
                            #
                            if 0:
                                log( 'functions that take struct args and are not used exactly once in methods:')
                                num = 0
                                for name in sorted( fn_usage.keys()):
                                    if name in omit_class_names:
                                        continue
                                    n, cursor = fn_usage[ name]
                                    if n == 1:
                                        continue
                                    if not fn_has_struct_args( tu, cursor):
                                        continue
                                    log( '    {n} {cursor.displayname} -> {cursor.result_type.spelling}')
                                    num += 1
                                log( 'number of functions that we should maybe add wrappers for: {num}')

                        elif action == '1':
                            # Compile and link generated C++ code to create libmupdfcpp.so.
                            if not h_files:
                                raise Exception( 'action "0" required')

                            out_so = f'{build_dirs.dir_mupdf}platform/c++/libmupdfcpp.so'
                            if build_dirs.dir_so:
                                out_so = f'{build_dirs.dir_so}libmupdfcpp.so'

                            mupdf_so = f'{build_dirs.dir_so}libmupdf.so'

                            include1 = f'{build_dirs.dir_mupdf}include'
                            include2 = f'{build_dirs.dir_mupdf}platform/c++/include'
                            command = ( textwrap.dedent(
                                    f'''
                                    g++
                                        -o {out_so}
                                        {build_dirs.cpp_flags}
                                        -fPIC
                                        -shared
                                        -I {include1}
                                        -I {include2}
                                        {" ".join(cpp_files)}
                                        {jlib.link_l_flags(mupdf_so)}
                                    ''').strip().replace( '\n', ' \\\n')
                                    )
                            jlib.build(
                                    [include1, include2] + cpp_files,
                                    [out_so],
                                    command,
                                    force_rebuild,
                                    )

                        elif action == '2':
                            # Generate C++ code for python module using SWIG.
                            if not container_classnames:
                                raise Exception( 'action "0" required')
                            with jlib.LogPrefixScope( f'swig: '):
                                build_swig( build_dirs, container_classnames, swig=swig)

                        elif action == 'j':
                            # Just experimenting.
                            build_swig_java()

                        elif action == '3':
                            # Compile and link to create _mupdfcpp_swig.so.
                            #
                            # We use python3-config to find libpython.so and
                            # python-dev include path etc.
                            #
                            # We use g++ debug/release flags as implied by
                            # --dir-so, but all builds output the same file
                            # mupdf:platform/python/_mupdf.so. We could instead
                            # generate mupdf.py and _mupdf.so in the --dir-so
                            # directory?
                            #
                            # [While libmupdfcpp.so requires matching
                            # debug/release build of libmupdf.so, it looks
                            # like _mupdf.so does not require a matching
                            # libmupdfcpp.so and libmupdf.sp.]
                            #
                            python_configdir = jlib.system( 'python3-config --configdir', out='return')
                            libpython_so = os.path.join(
                                    python_configdir.strip(),
                                    f'libpython{sys.version_info[0]}.{sys.version_info[1]}.so',
                                    )
                            assert os.path.isfile( libpython_so), f'cannot find libpython_so={libpython_so}'

                            python_includes = jlib.system( 'python3-config --includes', out='return')
                            python_includes = python_includes.strip()

                            # These are the input files to our g++ command:
                            #
                            mupdfcpp_swig_cpp   = f'{build_dirs.dir_mupdf}platform/python/mupdfcpp_swig.cpp'
                            include1            = f'{build_dirs.dir_mupdf}include'
                            include2            = f'{build_dirs.dir_mupdf}platform/c++/include'

                            mupdf_so            = f'{build_dirs.dir_so}libmupdf.so'
                            mupdfcpp_so         = f'{build_dirs.dir_so}libmupdfcpp.so'

                            # Python expects _mupdf.so to be in same directory as mupdf.py.
                            #
                            out_so              = f'{build_dirs.dir_so}_mupdf.so'

                            # We use jlib.link_l_flags() to add -L options
                            # to search parent directories of each .so that
                            # we need, and -l with the .so leafname without
                            # leading 'lib' or trailing '.so'. This ensures
                            # that at runtime one can set LD_LIBRARY_PATH to
                            # parent directories and have everything work.
                            #
                            command = ( textwrap.dedent(
                                    f'''
                                    g++
                                        -o {out_so}
                                        {build_dirs.cpp_flags}
                                        -fPIC
                                        --shared
                                        -I {include1}
                                        -I {include2}
                                        {python_includes}
                                        {mupdfcpp_swig_cpp}
                                        {jlib.link_l_flags( [mupdf_so, mupdfcpp_so, libpython_so])}
                                    ''').strip().replace( '\n', ' \\\n').strip()
                                    )
                            jlib.build(
                                    ( include1, include2, mupdfcpp_swig_cpp, mupdf_so, mupdfcpp_so,),
                                    ( out_so,),
                                    command,
                                    force_rebuild,
                                    )

                        else:
                            raise Exception( 'unrecognised --build action %r' % action)

            elif arg == '--compare-fz_usage':
                directory = args.next()
                compare_fz_usage( tu, directory, fn_usage)

            elif arg == '--diff':
                for path in jlib.get_filenames( build_dirs.ref_dir):
                    #log( '{path=}')
                    assert path.startswith( build_dirs.ref_dir)
                    if not path.endswith( '.h') and not path.endswith( '.cpp'):
                        continue
                    tail = path[ len( build_dirs.ref_dir):]
                    path2 = f'{build_dirs.dir_mupdf}platform/c++/{tail}'
                    command = f'diff -u {path} {path2}'
                    log( 'running: {command}')
                    jlib.system(
                            command,
                            raise_errors=False,
                            out=lambda text: log( text, nv=False),
                            )

            elif arg == '--diff-all':
                for a, b in (
                        (f'{build_dirs.dir_mupdf}platform/c++/', f'{build_dirs.dir_mupdf}platform/c++/'),
                        (f'{build_dirs.dir_mupdf}platform/python/', f'{build_dirs.dir_mupdf}platform/python/')
                        ):
                    for dirpath, dirnames, filenames in os.walk( a):
                        assert dirpath.startswith( a)
                        root = dirpath[len(a):]
                        for filename in filenames:
                            a_path = os.path.join(dirpath, filename)
                            b_path = os.path.join( b, root, filename)
                            command = f'diff -u {a_path} {b_path}'
                            jlib.system( command, raise_errors=False)

            elif arg == '--doc':

                def do_doxygen( name, outdir, path):
                    '''
                    name:
                        Doxygen PROJECT_NAME of generated documentation
                    outdir:
                        Directory in which we run doxygen, so root of generated
                        documentation will be in <outdir>/html/index.html
                    path:
                        Doxygen INPUT setting; this is the path relative to
                        <outdir> of the directory which contains the API to
                        document.
                    '''
                    # We generate a blank doxygen configuration file, make
                    # some minimal changes, then run doxygen on the modified
                    # configuration.
                    #
                    dname = f'{name}.doxygen'
                    dname2 = os.path.join( outdir, dname)
                    jlib.system( f'cd {outdir}; rm -f {dname}0; doxygen -g {dname}0', out='return')
                    with open( dname2+'0') as f:
                        dtext = f.read()
                    dtext, n = re.subn( '\nPROJECT_NAME *=.*\n', f'\nPROJECT_NAME = {name}\n', dtext)
                    assert n == 1
                    dtext, n = re.subn( '\nEXTRACT_ALL *=.*\n', f'\nEXTRACT_ALL = YES\n', dtext)
                    assert n == 1
                    dtext, n = re.subn( '\nINPUT *=.*\n', f'\nINPUT = {path}\n', dtext)
                    assert n == 1
                    dtext, n = re.subn( '\nRECURSIVE *=.*\n', f'\nRECURSIVE = YES\n', dtext)
                    with open( dname2, 'w') as f:
                        f.write( dtext)
                    #jlib.system( f'diff -u {dname2}0 {dname2}', raise_errors=False)
                    command = f'cd {outdir}; doxygen {dname}'
                    log( 'running: {command}')
                    jlib.system( command, out='return')
                    log( 'have created: {outdir}/html/index.html')

                langs = args.next()
                if langs == 'all':
                    langs = 'c,c++,python'

                langs = langs.split( ',')

                for lang in langs:
                    if lang == 'c':
                        do_doxygen( 'mupdf', 'include', 'mupdf')

                    elif lang == 'c++':
                        do_doxygen( 'mupdfcpp', 'platform/c++/include', 'mupdf')

                    elif lang == 'python':
                        ld_library_path = os.path.abspath( f'{build_dirs.dir_so}')
                        pythonpath = build_dirs.dir_so
                        jlib.system( f'cd {build_dirs.dir_so}; LD_LIBRARY_PATH={ld_library_path} PYTHONPATH={pythonpath} pydoc3 -w ./mupdf.py')
                        assert os.path.isfile( f'{build_dirs.dir_so}mupdf.html')

                    else:
                        raise Exception( f'unrecognised language param: {lang}')

            elif arg == '-h' or arg == '--help':
                print( __doc__)

            elif arg == '--ref':
                assert 'mupdfwrap_ref' in build_dirs.ref_dir
                jlib.system(
                        f'rm -r {build_dirs.ref_dir}',
                        raise_errors=False,
                        out=lambda text: log(text),
                        )
                jlib.system(
                        f'rsync -ai {build_dirs.dir_mupdf}platform/c++/implementation {build_dirs.ref_dir}',
                        out=lambda text: log(text),
                        )
                jlib.system(
                        f'rsync -ai {build_dirs.dir_mupdf}platform/c++/include {build_dirs.ref_dir}',
                        out=lambda text: log(text),
                        )

            elif arg == '--dir-so' or arg == '-d':
                d = args.next()
                build_dirs.set_dir_so( d)

            elif arg == '--run-py':
                command = ''
                while 1:
                    try:
                        command += ' ' + args.next()
                    except StopIteration:
                        break

                ld_library_path = os.path.abspath( f'{build_dirs.dir_so}')
                pythonpath = build_dirs.dir_so

                envs = f'LD_LIBRARY_PATH={ld_library_path} PYTHONPATH={pythonpath}'
                command = f'{envs} {command}'
                log( 'running: {command}')
                e = jlib.system(
                        command,
                        out = -1,
                        raise_errors=False,
                        verbose=False,
                        )
                sys.exit(e)

            elif arg == '--swig':
                swig = args.next()

            elif arg == '--sync':
                sync_docs = False
                destination = args.next()
                if destination == '-d':
                    sync_docs = True
                    destination = args.next()
                log( 'Syncing to {destination=}')
                txts = []
                files = list( hs) + list( cpps) + list( txts) + [
                        f'{build_dirs.dir_mupdf}platform/python/mupdf_test-out2.png',
                        f'{build_dirs.dir_mupdf}platform/python/mupdf_test-out3.png',
                        f'{build_dirs.dir_mupdf}platform/python/mupdf_test-out.png',
                        f'{build_dirs.dir_mupdf}platform/python/mupdf_test-out.ppm',
                        f'{build_dirs.dir_mupdf}platform/python/mupdf_test.py',
                        f'{build_dirs.dir_mupdf}platform/python/mupdf_test.py.out.txt',
                        f'{build_dirs.dir_so}mupdf.py',
                        f'{build_dirs.dir_mupdf}platform/c++/fn_usage.txt',
                        ]
                # Generate .html files with syntax colouring for source files. See:
                #   https://github.com/google/code-prettify
                #
                files_html = []
                for i in files:
                    if os.path.splitext( i)[1] not in ( '.h', '.cpp', '.py'):
                        continue
                    o = f'{i}.html'
                    log( 'converting {i} to {o}')
                    with open( i) as f:
                        text = f.read()
                    with open( o, 'w') as f:
                        f.write( '<html><body>\n')
                        f.write( '<script src="https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js"></script>\n')
                        f.write( '<pre class="prettyprint">\n')
                        f.write( text)
                        f.write( '</pre>\n')
                        f.write( '</body></html>\n')
                    files_html.append( o)

                files += files_html

                if sync_docs:
                    files += [
                            f'{build_dirs.dir_mupdf}include/html/',
                            f'{build_dirs.dir_mupdf}platform/c++/include/html/',
                            f'{build_dirs.dir_so}mupdf.html',
                            ]

                # Insert extra './' into each path so that rsync -R uses the
                # 'mupdf/...' tail of each local path for the remote path.
                #
                for i in range( len( files)):
                    files[i] = files[i].replace( '/mupdf/', '/./mupdf/')

                jlib.system( f'rsync -aiRz {" ".join( files)} {destination}', verbose=1)


            elif arg == '--test' or arg == '-t':

                # Create test .py programme.
                #
                with open( f'{build_dirs.dir_mupdf}platform/python/mupdf_test.py', 'w') as out:
                    os.fchmod( out.fileno(), 0o755)
                    out.write( '#!/usr/bin/env python3\n')
                    out.write( inspect.getsource( test_mupdfcpp_swig))
                    out.write( textwrap.dedent( '''
                            if __name__ == '__main__':
                                test_mupdfcpp_swig()
                            '''))

                # We need to set LD_LIBRARY_PATH and PYTHONPATH so that our
                # test .py programme can load mupdf.py and _mupdf.so.
                ld_library_path = os.path.abspath( f'{build_dirs.dir_so}')
                pythonpath = build_dirs.dir_so

                envs = f'LD_LIBRARY_PATH={ld_library_path} PYTHONPATH={pythonpath}'

                log( 'running mupdf_test.py...')

                with open( f'{build_dirs.dir_mupdf}platform/python/mupdf_test.py.out.txt', 'w') as f:

                    def outfn( text):
                        log( text, nv=0)
                        f.write( text)

                    jlib.system(
                            f'cd {build_dirs.dir_mupdf}platform/python/; {envs} ./mupdf_test.py',
                            out = outfn
                            )

                # Run mutool.py.
                #
                mutool_py = os.path.abspath( __file__ + '/../mutool.py')
                log( 'running {mutool_py=}')
                jlib.system(
                        f'{envs} {mutool_py}',
                        out = lambda text: log( text),
                        )

            elif arg == '--test-swig':
                test_swig()

            else:
                raise Exception( f'unrecognised arg: {arg}')


if __name__ == '__main__':
    jlib.force_line_buffering()
    try:
        main()
    except Exception:
        print( jlib.exception_info())
